diff --git a/.travis.yml b/.travis.yml index 43dc4e7bef..f35f3546de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,5 +14,3 @@ before_script: - echo '' > ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini - echo "extension = apc.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - COMPOSER_ROOT_VERSION=dev-master composer --prefer-source --dev install - - php src/Symfony/Component/Locale/Resources/data/build-data.php - - export USE_INTL_ICU_DATA_VERSION=1 diff --git a/autoload.php.dist b/autoload.php.dist index 2be61eb189..0fc5b156f3 100644 --- a/autoload.php.dist +++ b/autoload.php.dist @@ -10,8 +10,4 @@ $loader = require_once __DIR__.'/vendor/autoload.php'; use Doctrine\Common\Annotations\AnnotationRegistry; -if (!function_exists('intl_get_error_code')) { - require_once __DIR__.'/src/Symfony/Component/Locale/Resources/stubs/functions.php'; -} - AnnotationRegistry::registerLoader(array($loader, 'loadClass')); diff --git a/composer.json b/composer.json index b6c0366d63..d4a1f932be 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ ], "require": { "php": ">=5.3.3", + "symfony/icu": ">=1.0", "doctrine/common": "~2.2", "twig/twig": "~1.11", "psr/log": "~1.0" @@ -37,6 +38,7 @@ "symfony/framework-bundle": "self.version", "symfony/http-foundation": "self.version", "symfony/http-kernel": "self.version", + "symfony/intl": "self.version", "symfony/locale": "self.version", "symfony/monolog-bridge": "self.version", "symfony/options-resolver": "self.version", @@ -68,8 +70,9 @@ "psr-0": { "Symfony\\": "src/" }, "classmap": [ "src/Symfony/Component/HttpFoundation/Resources/stubs", - "src/Symfony/Component/Locale/Resources/stubs" - ] + "src/Symfony/Component/Intl/Resources/stubs" + ], + "files": [ "src/Symfony/Component/Intl/Resources/stubs/functions.php" ] }, "minimum-stability": "dev", "extra": { diff --git a/src/Symfony/Component/Intl/.gitignore b/src/Symfony/Component/Intl/.gitignore new file mode 100644 index 0000000000..c49a5d8df5 --- /dev/null +++ b/src/Symfony/Component/Intl/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Intl/CONTRIBUTING.md b/src/Symfony/Component/Intl/CONTRIBUTING.md new file mode 100644 index 0000000000..b290393346 --- /dev/null +++ b/src/Symfony/Component/Intl/CONTRIBUTING.md @@ -0,0 +1,91 @@ +Contributing to the Intl component +================================== + +A very good way of contributing to the Intl component is by updating the +included data for the ICU version you have installed on your system. + +Preparation +----------- + +To prepare, you need to install the development dependencies of the component. + + $ cd /path/to/Symfony/Component/Intl + $ composer.phar install --dev + +Determining your ICU version +--------------------------- + +The ICU version installed in your PHP environment can be found by running +icu-version.php: + + $ php Resources/bin/icu-version.php + +Updating the ICU data +--------------------- + +To update the data files, run the update-icu-component.php script: + + $ php Resources/bin/update-icu-component.php + +The script needs the binaries "svn" and "make" to be available on your system. +It will download the latest version of the ICU sources for the ICU version +installed in your PHP environment. The script will then compile the "genrb" +binary and use it to compile the ICU data files to binaries. The binaries are +copied to the Resources/ directory of the Icu component found in the +vendor/symfony/icu/ directory. + +Updating the stub data +---------------------- + +In the previous step you updated the Icu component for the ICU version +installed on your system. If you are using the latest ICU version, you should +also create the stub data files which will be used by people who don't have +the intl extension installed. + +To update the stub files, run the update-stubs.php script: + + $ php Resources/bin/update-stubs.php + +The script will fail if you don't have the latest ICU version. If you want to +upgrade the ICU version, adjust the return value of the +`Intl::getStubIcuVersion()` before you run the script. + +The script creates copies of the binary resource bundles in the Icu component +and stores them in the Resources/ directory of the Intl component. The copies +are made for the locale "en" only and are stored in .php files, so that they +can be read even if the intl extension is not available. + +Creating a pull request +----------------------- + +You need to create up to two pull requests: + +* If you updated the Icu component, you need to push that change and create a + pull request in the `symfony/Icu` repository. Make sure to submit the pull + request to the correct master branch. If you updated the ICU data for version + 4.8, your pull request goes to branch `48-master`, for version 49 to + `49-master` and so on. + +* If you updated the stub files of the Intl component, you need to push that + change and create a pull request in the `symfony/symfony` repository. The + pull request should be based on the `master` branch. + +Combining .res files to a .dat-package +-------------------------------------- + +The individual *.res files can be combined into a single .dat-file. +Unfortunately, PHP's `ResourceBundle` class is currently not able to handle +.dat-files. + +Once it is, the following steps have to be followed to build the .dat-file: + +1. Package the resource bundles into a single file + + $ find . -name *.res | sed -e "s/\.\///g" > packagelist.txt + $ pkgdata -p region -T build -d . packagelist.txt + +2. Clean up + + $ rm -rf build packagelist.txt + +3. You can now move region.dat to replace the version bundled with Symfony2. diff --git a/src/Symfony/Component/Intl/Collator/StubCollator.php b/src/Symfony/Component/Intl/Collator/StubCollator.php new file mode 100644 index 0000000000..6ed4904b93 --- /dev/null +++ b/src/Symfony/Component/Intl/Collator/StubCollator.php @@ -0,0 +1,284 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Collator; + +use Symfony\Component\Intl\Exception\MethodNotImplementedException; +use Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException; +use Symfony\Component\Intl\Globals\StubIntlGlobals; +use Symfony\Component\Intl\Locale\StubLocale; + +/** + * Provides a stub Collator for the 'en' locale. + * + * @author Igor Wiedler + */ +class StubCollator +{ + /** Attribute constants */ + const FRENCH_COLLATION = 0; + const ALTERNATE_HANDLING = 1; + const CASE_FIRST = 2; + const CASE_LEVEL = 3; + const NORMALIZATION_MODE = 4; + const STRENGTH = 5; + const HIRAGANA_QUATERNARY_MODE = 6; + const NUMERIC_COLLATION = 7; + + /** Attribute constants values */ + const DEFAULT_VALUE = -1; + + const PRIMARY = 0; + const SECONDARY = 1; + const TERTIARY = 2; + const DEFAULT_STRENGTH = 2; + const QUATERNARY = 3; + const IDENTICAL = 15; + + const OFF = 16; + const ON = 17; + + const SHIFTED = 20; + const NON_IGNORABLE = 21; + + const LOWER_FIRST = 24; + const UPPER_FIRST = 25; + + /** Sorting options */ + const SORT_REGULAR = 0; + const SORT_NUMERIC = 2; + const SORT_STRING = 1; + + /** + * Constructor + * + * @param string $locale The locale code + * + * @throws MethodArgumentValueNotImplementedException When $locale different than 'en' is passed + */ + public function __construct($locale) + { + if ('en' != $locale) { + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'locale', $locale, 'Only the \'en\' locale is supported'); + } + } + + /** + * Static constructor + * + * @param string $locale The locale code + * + * @return StubCollator + * + * @throws MethodArgumentValueNotImplementedException When $locale different than 'en' is passed + */ + public static function create($locale) + { + return new self($locale); + } + + /** + * Sort array maintaining index association + * + * @param array &$array Input array + * @param integer $sortFlag Flags for sorting, can be one of the following: + * StubCollator::SORT_REGULAR - compare items normally (don't change types) + * StubCollator::SORT_NUMERIC - compare items numerically + * StubCollator::SORT_STRING - compare items as strings + * + * @return Boolean True on success or false on failure + */ + public function asort(&$array, $sortFlag = self::SORT_REGULAR) + { + $intlToPlainFlagMap = array( + self::SORT_REGULAR => \SORT_REGULAR, + self::SORT_NUMERIC => \SORT_NUMERIC, + self::SORT_STRING => \SORT_STRING, + ); + + $plainSortFlag = isset($intlToPlainFlagMap[$sortFlag]) ? $intlToPlainFlagMap[$sortFlag] : self::SORT_REGULAR; + + return asort($array, $plainSortFlag); + } + + /** + * Compare two Unicode strings + * + * @param string $str1 The first string to compare + * @param string $str2 The second string to compare + * + * @return Boolean|int Return the comparison result or false on failure: + * 1 if $str1 is greater than $str2 + * 0 if $str1 is equal than $str2 + * -1 if $str1 is less than $str2 + * + * @see http://www.php.net/manual/en/collator.compare.php + * + * @throws MethodNotImplementedException + */ + public function compare($str1, $str2) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Get a value of an integer collator attribute + * + * @param int $attr An attribute specifier, one of the attribute constants + * + * @return Boolean|int The attribute value on success or false on error + * + * @see http://www.php.net/manual/en/collator.getattribute.php + * + * @throws MethodNotImplementedException + */ + public function getAttribute($attr) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns collator's last error code. Always returns the U_ZERO_ERROR class constant value + * + * @return int The error code from last collator call + */ + public function getErrorCode() + { + return StubIntlGlobals::U_ZERO_ERROR; + } + + /** + * Returns collator's last error message. Always returns the U_ZERO_ERROR_MESSAGE class constant value + * + * @return string The error message from last collator call + */ + public function getErrorMessage() + { + return 'U_ZERO_ERROR'; + } + + /** + * Returns the collator's locale + * + * @param int $type The locale name type to return between valid or actual (StubLocale::VALID_LOCALE or StubLocale::ACTUAL_LOCALE, respectively) + * + * @return string The locale name used to create the collator + */ + public function getLocale($type = StubLocale::ACTUAL_LOCALE) + { + return 'en'; + } + + /** + * Get sorting key for a string + * + * @param string $string The string to produce the key from + * + * @return string The collation key for $string + * + * @see http://www.php.net/manual/en/collator.getsortkey.php + * + * @throws MethodNotImplementedException + */ + public function getSortKey($string) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Get current collator's strength + * + * @return Boolean|int The current collator's strength or false on failure + * + * @see http://www.php.net/manual/en/collator.getstrength.php + * + * @throws MethodNotImplementedException + */ + public function getStrength() + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Set a collator's attribute + * + * @param int $attr An attribute specifier, one of the attribute constants + * @param int $val The attribute value, one of the attribute value constants + * + * @return Boolean True on success or false on failure + * + * @see http://www.php.net/manual/en/collator.setattribute.php + * + * @throws MethodNotImplementedException + */ + public function setAttribute($attr, $val) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Set the collator's strength + * + * @param int $strength Strength to set, possible values: + * StubCollator::PRIMARY + * StubCollator::SECONDARY + * StubCollator::TERTIARY + * StubCollator::QUATERNARY + * StubCollator::IDENTICAL + * StubCollator::DEFAULT + * + * @return Boolean True on success or false on failure + * + * @see http://www.php.net/manual/en/collator.setstrength.php + * + * @throws MethodNotImplementedException + */ + public function setStrength($strength) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Sort array using specified collator and sort keys + * + * @param array &$arr Array of strings to sort + * + * @return Boolean True on success or false on failure + * + * @see http://www.php.net/manual/en/collator.sortwithsortkeys.php + * + * @throws MethodNotImplementedException + */ + public function sortWithSortKeys(&$arr) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Sort array using specified collator + * + * @param array &$arr Array of string to sort + * @param int $sortFlag Optional sorting type, one of the following: + * StubCollator::SORT_REGULAR + * StubCollator::SORT_NUMERIC + * StubCollator::SORT_STRING + * + * @return Boolean True on success or false on failure + * + * @see http://www.php.net/manual/en/collator.sort.php + * + * @throws MethodNotImplementedException + */ + public function sort(&$arr, $sortFlag = self::SORT_REGULAR) + { + throw new MethodNotImplementedException(__METHOD__); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/AmPmTransformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/AmPmTransformer.php new file mode 100644 index 0000000000..1a9601d8e9 --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/AmPmTransformer.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Parser and formatter for AM/PM markers format + * + * @author Igor Wiedler + */ +class AmPmTransformer extends Transformer +{ + /** + * {@inheritDoc} + */ + public function format(\DateTime $dateTime, $length) + { + return $dateTime->format('A'); + } + + /** + * {@inheritDoc} + */ + public function getReverseMatchingRegExp($length) + { + return 'AM|PM'; + } + + /** + * {@inheritDoc} + */ + public function extractDateOptions($matched, $length) + { + return array( + 'marker' => $matched + ); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/DayOfWeekTransformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/DayOfWeekTransformer.php new file mode 100644 index 0000000000..ee53a4e6d0 --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/DayOfWeekTransformer.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Parser and formatter for day of week format + * + * @author Igor Wiedler + */ +class DayOfWeekTransformer extends Transformer +{ + /** + * {@inheritDoc} + */ + public function format(\DateTime $dateTime, $length) + { + $dayOfWeek = $dateTime->format('l'); + switch ($length) { + case 4: + return $dayOfWeek; + case 5: + return $dayOfWeek[0]; + default: + return substr($dayOfWeek, 0, 3); + } + } + + /** + * {@inheritDoc} + */ + public function getReverseMatchingRegExp($length) + { + switch ($length) { + case 4: + return 'Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday'; + case 5: + return '[MTWFS]'; + default: + return 'Mon|Tue|Wed|Thu|Fri|Sat|Sun'; + } + } + + /** + * {@inheritDoc} + */ + public function extractDateOptions($matched, $length) + { + return array(); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/DayOfYearTransformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/DayOfYearTransformer.php new file mode 100644 index 0000000000..2c33888c5f --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/DayOfYearTransformer.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Parser and formatter for day of year format + * + * @author Igor Wiedler + */ +class DayOfYearTransformer extends Transformer +{ + /** + * {@inheritDoc} + */ + public function format(\DateTime $dateTime, $length) + { + $dayOfYear = $dateTime->format('z') + 1; + + return $this->padLeft($dayOfYear, $length); + } + + /** + * {@inheritDoc} + */ + public function getReverseMatchingRegExp($length) + { + return '\d{'.$length.'}'; + } + + /** + * {@inheritDoc} + */ + public function extractDateOptions($matched, $length) + { + return array(); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/DayTransformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/DayTransformer.php new file mode 100644 index 0000000000..19d30e746c --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/DayTransformer.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Parser and formatter for day format + * + * @author Igor Wiedler + */ +class DayTransformer extends Transformer +{ + /** + * {@inheritDoc} + */ + public function format(\DateTime $dateTime, $length) + { + return $this->padLeft($dateTime->format('j'), $length); + } + + /** + * {@inheritDoc} + */ + public function getReverseMatchingRegExp($length) + { + return 1 === $length ? '\d{1,2}' : '\d{'.$length.'}'; + } + + /** + * {@inheritDoc} + */ + public function extractDateOptions($matched, $length) + { + return array( + 'day' => (int) $matched, + ); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/FullTransformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/FullTransformer.php new file mode 100644 index 0000000000..7b760f1159 --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/FullTransformer.php @@ -0,0 +1,356 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +use Symfony\Component\Intl\Exception\NotImplementedException; +use Symfony\Component\Intl\Globals\StubIntlGlobals; +use Symfony\Component\Intl\DateFormatter\DateFormat\MonthTransformer; + +/** + * Parser and formatter for date formats + * + * @author Igor Wiedler + */ +class FullTransformer +{ + private $quoteMatch = "'(?:[^']+|'')*'"; + private $implementedChars = 'MLydQqhDEaHkKmsz'; + private $notImplementedChars = 'GYuwWFgecSAZvVW'; + private $regExp; + + /** + * @var Transformer[] + */ + private $transformers; + + private $pattern; + private $timezone; + + /** + * Constructor + * + * @param string $pattern The pattern to be used to format and/or parse values + * @param string $timezone The timezone to perform the date/time calculations + */ + public function __construct($pattern, $timezone) + { + $this->pattern = $pattern; + $this->timezone = $timezone; + + $implementedCharsMatch = $this->buildCharsMatch($this->implementedChars); + $notImplementedCharsMatch = $this->buildCharsMatch($this->notImplementedChars); + $this->regExp = "/($this->quoteMatch|$implementedCharsMatch|$notImplementedCharsMatch)/"; + + $this->transformers = array( + 'M' => new MonthTransformer(), + 'L' => new MonthTransformer(), + 'y' => new YearTransformer(), + 'd' => new DayTransformer(), + 'q' => new QuarterTransformer(), + 'Q' => new QuarterTransformer(), + 'h' => new Hour1201Transformer(), + 'D' => new DayOfYearTransformer(), + 'E' => new DayOfWeekTransformer(), + 'a' => new AmPmTransformer(), + 'H' => new Hour2400Transformer(), + 'K' => new Hour1200Transformer(), + 'k' => new Hour2401Transformer(), + 'm' => new MinuteTransformer(), + 's' => new SecondTransformer(), + 'z' => new TimeZoneTransformer(), + ); + } + + /** + * Return the array of Transformer objects + * + * @return Transformer[] Associative array of Transformer objects (format char => Transformer) + */ + public function getTransformers() + { + return $this->transformers; + } + + /** + * Format a DateTime using ICU dateformat pattern + * + * @param \DateTime $dateTime A DateTime object to be used to generate the formatted value + * + * @return string The formatted value + */ + public function format(\DateTime $dateTime) + { + $that = $this; + + $formatted = preg_replace_callback($this->regExp, function($matches) use ($that, $dateTime) { + return $that->formatReplace($matches[0], $dateTime); + }, $this->pattern); + + return $formatted; + } + + /** + * Return the formatted ICU value for the matched date characters + * + * @param string $dateChars The date characters to be replaced with a formatted ICU value + * @param DateTime $dateTime A DateTime object to be used to generate the formatted value + * + * @return string The formatted value + * + * @throws NotImplementedException When it encounters a not implemented date character + */ + public function formatReplace($dateChars, $dateTime) + { + $length = strlen($dateChars); + + if ($this->isQuoteMatch($dateChars)) { + return $this->replaceQuoteMatch($dateChars); + } + + if (isset($this->transformers[$dateChars[0]])) { + $transformer = $this->transformers[$dateChars[0]]; + + return $transformer->format($dateTime, $length); + } + + // handle unimplemented characters + if (false !== strpos($this->notImplementedChars, $dateChars[0])) { + throw new NotImplementedException(sprintf("Unimplemented date character '%s' in format '%s'", $dateChars[0], $this->pattern)); + } + } + + /** + * Parse a pattern based string to a timestamp value + * + * @param \DateTime $dateTime A configured DateTime object to use to perform the date calculation + * @param string $value String to convert to a time value + * + * @return int The corresponding Unix timestamp + * + * @throws \InvalidArgumentException When the value can not be matched with pattern + */ + public function parse(\DateTime $dateTime, $value) + { + $reverseMatchingRegExp = $this->getReverseMatchingRegExp($this->pattern); + $reverseMatchingRegExp = '/^'.$reverseMatchingRegExp.'$/'; + + $options = array(); + + if (preg_match($reverseMatchingRegExp, $value, $matches)) { + $matches = $this->normalizeArray($matches); + + foreach ($this->transformers as $char => $transformer) { + if (isset($matches[$char])) { + $length = strlen($matches[$char]['pattern']); + $options = array_merge($options, $transformer->extractDateOptions($matches[$char]['value'], $length)); + } + } + + // reset error code and message + StubIntlGlobals::setError(StubIntlGlobals::U_ZERO_ERROR); + + return $this->calculateUnixTimestamp($dateTime, $options); + } + + // behave like the intl extension + StubIntlGlobals::setError(StubIntlGlobals::U_PARSE_ERROR, 'Date parsing failed'); + + return false; + } + + /** + * Retrieve a regular expression to match with a formatted value. + * + * @param string $pattern The pattern to create the reverse matching regular expression + * + * @return string The reverse matching regular expression with named captures being formed by the + * transformer index in the $transformer array + */ + public function getReverseMatchingRegExp($pattern) + { + $that = $this; + + $escapedPattern = preg_quote($pattern, '/'); + + // ICU 4.8 recognizes slash ("/") in a value to be parsed as a dash ("-") and vice-versa + // when parsing a date/time value + $escapedPattern = preg_replace('/\\\[\-|\/]/', '[\/\-]', $escapedPattern); + + $reverseMatchingRegExp = preg_replace_callback($this->regExp, function($matches) use ($that) { + $length = strlen($matches[0]); + $transformerIndex = $matches[0][0]; + + $dateChars = $matches[0]; + if ($that->isQuoteMatch($dateChars)) { + return $that->replaceQuoteMatch($dateChars); + } + + $transformers = $that->getTransformers(); + if (isset($transformers[$transformerIndex])) { + $transformer = $transformers[$transformerIndex]; + $captureName = str_repeat($transformerIndex, $length); + + return "(?P<$captureName>".$transformer->getReverseMatchingRegExp($length).')'; + } + }, $escapedPattern); + + return $reverseMatchingRegExp; + } + + /** + * Check if the first char of a string is a single quote + * + * @param string $quoteMatch The string to check + * + * @return Boolean true if matches, false otherwise + */ + public function isQuoteMatch($quoteMatch) + { + return ("'" === $quoteMatch[0]); + } + + /** + * Replaces single quotes at the start or end of a string with two single quotes + * + * @param string $quoteMatch The string to replace the quotes + * + * @return string A string with the single quotes replaced + */ + public function replaceQuoteMatch($quoteMatch) + { + if (preg_match("/^'+$/", $quoteMatch)) { + return str_replace("''", "'", $quoteMatch); + } + + return str_replace("''", "'", substr($quoteMatch, 1, -1)); + } + + /** + * Builds a chars match regular expression + * + * @param string $specialChars A string of chars to build the regular expression + * + * @return string The chars match regular expression + */ + protected function buildCharsMatch($specialChars) + { + $specialCharsArray = str_split($specialChars); + + $specialCharsMatch = implode('|', array_map(function($char) { + return $char.'+'; + }, $specialCharsArray)); + + return $specialCharsMatch; + } + + /** + * Normalize a preg_replace match array, removing the numeric keys and returning an associative array + * with the value and pattern values for the matched Transformer + * + * @param array $data + * + * @return array + */ + protected function normalizeArray(array $data) + { + $ret = array(); + + foreach ($data as $key => $value) { + if (!is_string($key)) { + continue; + } + + $ret[$key[0]] = array( + 'value' => $value, + 'pattern' => $key + ); + } + + return $ret; + } + + /** + * Calculates the Unix timestamp based on the matched values by the reverse matching regular + * expression of parse() + * + * @param \DateTime $dateTime The DateTime object to be used to calculate the timestamp + * @param array $options An array with the matched values to be used to calculate the timestamp + * + * @return Boolean|int The calculated timestamp or false if matched date is invalid + */ + protected function calculateUnixTimestamp(\DateTime $dateTime, array $options) + { + $options = $this->getDefaultValueForOptions($options); + + $year = $options['year']; + $month = $options['month']; + $day = $options['day']; + $hour = $options['hour']; + $hourInstance = $options['hourInstance']; + $minute = $options['minute']; + $second = $options['second']; + $marker = $options['marker']; + $timezone = $options['timezone']; + + // If month is false, return immediately (intl behavior) + if (false === $month) { + StubIntlGlobals::setError(StubIntlGlobals::U_PARSE_ERROR, 'Date parsing failed'); + + return false; + } + + // Normalize hour + if ($hourInstance instanceof HourTransformer) { + $hour = $hourInstance->normalizeHour($hour, $marker); + } + + // Set the timezone if different from the default one + if (null !== $timezone && $timezone !== $this->timezone) { + $dateTime->setTimezone(new \DateTimeZone($timezone)); + } + + // Normalize yy year + preg_match_all($this->regExp, $this->pattern, $matches); + if (in_array('yy', $matches[0])) { + $dateTime->setTimestamp(time()); + $year = $year > $dateTime->format('y') + 20 ? 1900 + $year : 2000 + $year; + } + + $dateTime->setDate($year, $month, $day); + $dateTime->setTime($hour, $minute, $second); + + return $dateTime->getTimestamp(); + } + + /** + * Add sensible default values for missing items in the extracted date/time options array. The values + * are base in the beginning of the Unix era + * + * @param array $options + * + * @return array + */ + private function getDefaultValueForOptions(array $options) + { + return array( + 'year' => isset($options['year']) ? $options['year'] : 1970, + 'month' => isset($options['month']) ? $options['month'] : 1, + 'day' => isset($options['day']) ? $options['day'] : 1, + 'hour' => isset($options['hour']) ? $options['hour'] : 0, + 'hourInstance' => isset($options['hourInstance']) ? $options['hourInstance'] : null, + 'minute' => isset($options['minute']) ? $options['minute'] : 0, + 'second' => isset($options['second']) ? $options['second'] : 0, + 'marker' => isset($options['marker']) ? $options['marker'] : null, + 'timezone' => isset($options['timezone']) ? $options['timezone'] : null, + ); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/Hour1200Transformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/Hour1200Transformer.php new file mode 100644 index 0000000000..8c8f5ef497 --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/Hour1200Transformer.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Parser and formatter for 12 hour format (0-11) + * + * @author Igor Wiedler + */ +class Hour1200Transformer extends HourTransformer +{ + /** + * {@inheritDoc} + */ + public function format(\DateTime $dateTime, $length) + { + $hourOfDay = $dateTime->format('g'); + $hourOfDay = '12' == $hourOfDay ? '0' : $hourOfDay; + + return $this->padLeft($hourOfDay, $length); + } + + /** + * {@inheritDoc} + */ + public function normalizeHour($hour, $marker = null) + { + if ('PM' === $marker) { + $hour += 12; + } + + return $hour; + } + + /** + * {@inheritDoc} + */ + public function getReverseMatchingRegExp($length) + { + return '\d{1,2}'; + } + + /** + * {@inheritDoc} + */ + public function extractDateOptions($matched, $length) + { + return array( + 'hour' => (int) $matched, + 'hourInstance' => $this + ); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/Hour1201Transformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/Hour1201Transformer.php new file mode 100644 index 0000000000..a8c43702f0 --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/Hour1201Transformer.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Parser and formatter for 12 hour format (1-12) + * + * @author Igor Wiedler + */ +class Hour1201Transformer extends HourTransformer +{ + /** + * {@inheritDoc} + */ + public function format(\DateTime $dateTime, $length) + { + return $this->padLeft($dateTime->format('g'), $length); + } + + /** + * {@inheritDoc} + */ + public function normalizeHour($hour, $marker = null) + { + if ('PM' !== $marker && 12 === $hour) { + $hour = 0; + } elseif ('PM' === $marker && 12 !== $hour) { + // If PM and hour is not 12 (1-12), sum 12 hour + $hour += 12; + } + + return $hour; + } + + /** + * {@inheritDoc} + */ + public function getReverseMatchingRegExp($length) + { + return '\d{1,2}'; + } + + /** + * {@inheritDoc} + */ + public function extractDateOptions($matched, $length) + { + return array( + 'hour' => (int) $matched, + 'hourInstance' => $this + ); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/Hour2400Transformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/Hour2400Transformer.php new file mode 100644 index 0000000000..8f22da1336 --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/Hour2400Transformer.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Parser and formatter for 24 hour format (0-23) + * + * @author Igor Wiedler + */ +class Hour2400Transformer extends HourTransformer +{ + /** + * {@inheritDoc} + */ + public function format(\DateTime $dateTime, $length) + { + return $this->padLeft($dateTime->format('G'), $length); + } + + /** + * {@inheritDoc} + */ + public function normalizeHour($hour, $marker = null) + { + if ('AM' == $marker) { + $hour = 0; + } elseif ('PM' == $marker) { + $hour = 12; + } + + return $hour; + } + + /** + * {@inheritDoc} + */ + public function getReverseMatchingRegExp($length) + { + return '\d{1,2}'; + } + + /** + * {@inheritDoc} + */ + public function extractDateOptions($matched, $length) + { + return array( + 'hour' => (int) $matched, + 'hourInstance' => $this + ); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/Hour2401Transformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/Hour2401Transformer.php new file mode 100644 index 0000000000..b0f486b9da --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/Hour2401Transformer.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Parser and formatter for 24 hour format (1-24) + * + * @author Igor Wiedler + */ +class Hour2401Transformer extends HourTransformer +{ + /** + * {@inheritDoc} + */ + public function format(\DateTime $dateTime, $length) + { + $hourOfDay = $dateTime->format('G'); + $hourOfDay = ('0' == $hourOfDay) ? '24' : $hourOfDay; + + return $this->padLeft($hourOfDay, $length); + } + + /** + * {@inheritDoc} + */ + public function normalizeHour($hour, $marker = null) + { + if ((null === $marker && 24 === $hour) || 'AM' == $marker) { + $hour = 0; + } elseif ('PM' == $marker) { + $hour = 12; + } + + return $hour; + } + + /** + * {@inheritDoc} + */ + public function getReverseMatchingRegExp($length) + { + return '\d{1,2}'; + } + + /** + * {@inheritDoc} + */ + public function extractDateOptions($matched, $length) + { + return array( + 'hour' => (int) $matched, + 'hourInstance' => $this + ); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/HourTransformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/HourTransformer.php new file mode 100644 index 0000000000..51097d92b6 --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/HourTransformer.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Base class for hour transformers + * + * @author Eriksen Costa + */ +abstract class HourTransformer extends Transformer +{ + /** + * Returns a normalized hour value suitable for the hour transformer type + * + * @param int $hour The hour value + * @param string $marker An optional AM/PM marker + * + * @return int The normalized hour value + */ + abstract public function normalizeHour($hour, $marker = null); +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/MinuteTransformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/MinuteTransformer.php new file mode 100644 index 0000000000..b48de29203 --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/MinuteTransformer.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Parser and formatter for minute format + * + * @author Igor Wiedler + */ +class MinuteTransformer extends Transformer +{ + /** + * {@inheritDoc} + */ + public function format(\DateTime $dateTime, $length) + { + $minuteOfHour = (int) $dateTime->format('i'); + + return $this->padLeft($minuteOfHour, $length); + } + + /** + * {@inheritDoc} + */ + public function getReverseMatchingRegExp($length) + { + return 1 === $length ? '\d{1,2}' : '\d{'.$length.'}'; + } + + /** + * {@inheritDoc} + */ + public function extractDateOptions($matched, $length) + { + return array( + 'minute' => (int) $matched, + ); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/MonthTransformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/MonthTransformer.php new file mode 100644 index 0000000000..30c15af77e --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/MonthTransformer.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Parser and formatter for month format + * + * @author Igor Wiedler + */ +class MonthTransformer extends Transformer +{ + /** + * @var array + */ + protected static $months = array( + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ); + + /** + * Short months names (first 3 letters) + * @var array + */ + protected static $shortMonths = array(); + + /** + * Flipped $months array, $name => $index + * @var array + */ + protected static $flippedMonths = array(); + + /** + * Flipped $shortMonths array, $name => $index + * @var array + */ + protected static $flippedShortMonths = array(); + + /** + * Constructor + */ + public function __construct() + { + if (0 === count(self::$shortMonths)) { + self::$shortMonths = array_map(function($month) { + return substr($month, 0, 3); + }, self::$months); + + self::$flippedMonths = array_flip(self::$months); + self::$flippedShortMonths = array_flip(self::$shortMonths); + } + } + + /** + * {@inheritDoc} + */ + public function format(\DateTime $dateTime, $length) + { + $matchLengthMap = array( + 1 => 'n', + 2 => 'm', + 3 => 'M', + 4 => 'F', + ); + + if (isset($matchLengthMap[$length])) { + return $dateTime->format($matchLengthMap[$length]); + } + + if (5 === $length) { + return substr($dateTime->format('M'), 0, 1); + } + + return $this->padLeft($dateTime->format('m'), $length); + } + + /** + * {@inheritDoc} + */ + public function getReverseMatchingRegExp($length) + { + switch ($length) { + case 1: + $regExp = '\d{1,2}'; + break; + case 3: + $regExp = implode('|', self::$shortMonths); + break; + case 4: + $regExp = implode('|', self::$months); + break; + case 5: + $regExp = '[JFMASOND]'; + break; + default: + $regExp = '\d{'.$length.'}'; + break; + } + + return $regExp; + } + + /** + * {@inheritDoc} + */ + public function extractDateOptions($matched, $length) + { + if (!is_numeric($matched)) { + if (3 === $length) { + $matched = self::$flippedShortMonths[$matched] + 1; + } elseif (4 === $length) { + $matched = self::$flippedMonths[$matched] + 1; + } elseif (5 === $length) { + // IntlDateFormatter::parse() always returns false for MMMMM or LLLLL + $matched = false; + } + } else { + $matched = (int) $matched; + } + + return array( + 'month' => $matched, + ); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/QuarterTransformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/QuarterTransformer.php new file mode 100644 index 0000000000..8e83dc757f --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/QuarterTransformer.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Parser and formatter for quarter format + * + * @author Igor Wiedler + */ +class QuarterTransformer extends Transformer +{ + /** + * {@inheritDoc} + */ + public function format(\DateTime $dateTime, $length) + { + $month = (int) $dateTime->format('n'); + $quarter = (int) floor(($month - 1) / 3) + 1; + switch ($length) { + case 1: + case 2: + return $this->padLeft($quarter, $length); + case 3: + return 'Q'.$quarter; + default: + $map = array(1 => '1st quarter', 2 => '2nd quarter', 3 => '3rd quarter', 4 => '4th quarter'); + + return $map[$quarter]; + } + } + + /** + * {@inheritDoc} + */ + public function getReverseMatchingRegExp($length) + { + switch ($length) { + case 1: + case 2: + return '\d{'.$length.'}'; + case 3: + return 'Q\d'; + default: + return '(?:1st|2nd|3rd|4th) quarter'; + } + } + + /** + * {@inheritDoc} + */ + public function extractDateOptions($matched, $length) + { + return array(); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/SecondTransformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/SecondTransformer.php new file mode 100644 index 0000000000..ccbcdb4164 --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/SecondTransformer.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Parser and formatter for the second format + * + * @author Igor Wiedler + */ +class SecondTransformer extends Transformer +{ + /** + * {@inheritDoc} + */ + public function format(\DateTime $dateTime, $length) + { + $secondOfMinute = (int) $dateTime->format('s'); + + return $this->padLeft($secondOfMinute, $length); + } + + /** + * {@inheritDoc} + */ + public function getReverseMatchingRegExp($length) + { + return 1 === $length ? '\d{1,2}' : '\d{'.$length.'}'; + } + + /** + * {@inheritDoc} + */ + public function extractDateOptions($matched, $length) + { + return array( + 'second' => (int) $matched, + ); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/TimeZoneTransformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/TimeZoneTransformer.php new file mode 100644 index 0000000000..7d74bd3696 --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/TimeZoneTransformer.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +use Symfony\Component\Intl\Exception\NotImplementedException; + +/** + * Parser and formatter for time zone format + * + * @author Igor Wiedler + */ +class TimeZoneTransformer extends Transformer +{ + /** + * {@inheritDoc} + * + * @throws NotImplementedException When time zone is different than UTC or GMT (Etc/GMT) + */ + public function format(\DateTime $dateTime, $length) + { + $timeZone = substr($dateTime->getTimezone()->getName(), 0, 3); + + if (!in_array($timeZone, array('Etc', 'UTC'))) { + throw new NotImplementedException('Time zone different than GMT or UTC is not supported as a formatting output.'); + } + + // From ICU >= 4.8, the zero offset is not more used, example: GMT instead of GMT+00:00 + $format = (0 !== (int) $dateTime->format('O')) ? '\G\M\TP' : '\G\M\T'; + + return $dateTime->format($format); + } + + /** + * {@inheritDoc} + */ + public function getReverseMatchingRegExp($length) + { + return 'GMT[+-]\d{2}:?\d{2}'; + } + + /** + * {@inheritDoc} + */ + public function extractDateOptions($matched, $length) + { + return array( + 'timezone' => self::getEtcTimeZoneId($matched) + ); + } + + /** + * Get an Etc/GMT timezone identifier for the specified timezone + * + * The PHP documentation for timezones states to not use the 'Other' time zones because them exists + * "for backwards compatibility". However all Etc/GMT time zones are in the tz database 'etcetera' file, + * which indicates they are not deprecated (neither are old names). + * + * Only GMT, Etc/Universal, Etc/Zulu, Etc/Greenwich, Etc/GMT-0, Etc/GMT+0 and Etc/GMT0 are old names and + * are linked to Etc/GMT or Etc/UTC. + * + * @param string $formattedTimeZone A GMT timezone string (GMT-03:00, e.g.) + * + * @return string A timezone identifier + * + * @see http://php.net/manual/en/timezones.others.php + * @see http://www.twinsun.com/tz/tz-link.htm + * + * @throws NotImplementedException When the GMT time zone have minutes offset different than zero + * @throws \InvalidArgumentException When the value can not be matched with pattern + */ + public static function getEtcTimeZoneId($formattedTimeZone) + { + if (preg_match('/GMT(?P[+-])(?P\d{2}):?(?P\d{2})/', $formattedTimeZone, $matches)) { + $hours = (int) $matches['hours']; + $minutes = (int) $matches['minutes']; + $signal = $matches['signal'] == '-' ? '+' : '-'; + + if (0 < $minutes) { + throw new NotImplementedException(sprintf( + 'It is not possible to use a GMT time zone with minutes offset different than zero (0). GMT time zone tried: %s.', + $formattedTimeZone + )); + } + + return 'Etc/GMT'.($hours !== 0 ? $signal.$hours : ''); + } + + throw new \InvalidArgumentException('The GMT time zone \'%s\' does not match with the supported formats GMT[+-]HH:MM or GMT[+-]HHMM.'); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/Transformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/Transformer.php new file mode 100644 index 0000000000..0e67f8ae3f --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/Transformer.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Parser and formatter for date formats + * + * @author Igor Wiedler + */ +abstract class Transformer +{ + /** + * Format a value using a configured DateTime as date/time source + * + * + * @param \DateTime $dateTime A DateTime object to be used to generate the formatted value + * @param int $length The formatted value string length + * + * @return string The formatted value + */ + abstract public function format(\DateTime $dateTime, $length); + + /** + * Returns a reverse matching regular expression of a string generated by format() + * + * @param int $length The length of the value to be reverse matched + * + * @return string The reverse matching regular expression + */ + abstract public function getReverseMatchingRegExp($length); + + /** + * Extract date options from a matched value returned by the processing of the reverse matching + * regular expression + * + * @param string $matched The matched value + * @param int $length The length of the Transformer pattern string + * + * @return array An associative array + */ + abstract public function extractDateOptions($matched, $length); + + /** + * Pad a string with zeros to the left + * + * @param string $value The string to be padded + * @param int $length The length to pad + * + * @return string The padded string + */ + protected function padLeft($value, $length) + { + return str_pad($value, $length, '0', STR_PAD_LEFT); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/DateFormat/YearTransformer.php b/src/Symfony/Component/Intl/DateFormatter/DateFormat/YearTransformer.php new file mode 100644 index 0000000000..c3bd699dce --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/DateFormat/YearTransformer.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter\DateFormat; + +/** + * Parser and formatter for year format + * + * @author Igor Wiedler + */ +class YearTransformer extends Transformer +{ + /** + * {@inheritDoc} + */ + public function format(\DateTime $dateTime, $length) + { + if (2 === $length) { + return $dateTime->format('y'); + } + + return $this->padLeft($dateTime->format('Y'), $length); + } + + /** + * {@inheritDoc} + */ + public function getReverseMatchingRegExp($length) + { + return 2 === $length ? '\d{2}' : '\d{4}'; + } + + /** + * {@inheritDoc} + */ + public function extractDateOptions($matched, $length) + { + return array( + 'year' => (int) $matched, + ); + } +} diff --git a/src/Symfony/Component/Intl/DateFormatter/StubIntlDateFormatter.php b/src/Symfony/Component/Intl/DateFormatter/StubIntlDateFormatter.php new file mode 100644 index 0000000000..eb6efb80af --- /dev/null +++ b/src/Symfony/Component/Intl/DateFormatter/StubIntlDateFormatter.php @@ -0,0 +1,601 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\DateFormatter; + +use Symfony\Component\Intl\Globals\StubIntlGlobals; +use Symfony\Component\Intl\DateFormatter\DateFormat\FullTransformer; +use Symfony\Component\Intl\Exception\MethodNotImplementedException; +use Symfony\Component\Intl\Exception\MethodArgumentNotImplementedException; +use Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException; +use Symfony\Component\Intl\Locale\StubLocale; + +/** + * Provides a stub IntlDateFormatter for the 'en' locale. + * + * @author Igor Wiedler + */ +class StubIntlDateFormatter +{ + /** + * The error code from the last operation + * + * @var integer + */ + protected $errorCode = StubIntlGlobals::U_ZERO_ERROR; + + /** + * The error message from the last operation + * + * @var string + */ + protected $errorMessage = 'U_ZERO_ERROR'; + + /* date/time format types */ + const NONE = -1; + const FULL = 0; + const LONG = 1; + const MEDIUM = 2; + const SHORT = 3; + + /* calendar formats */ + const TRADITIONAL = 0; + const GREGORIAN = 1; + + /** + * Patterns used to format the date when no pattern is provided + * + * @var array + */ + private $defaultDateFormats = array( + self::NONE => '', + self::FULL => 'EEEE, LLLL d, y', + self::LONG => 'LLLL d, y', + self::MEDIUM => 'LLL d, y', + self::SHORT => 'M/d/yy', + ); + + /** + * Patterns used to format the time when no pattern is provided + * + * @var array + */ + private $defaultTimeFormats = array( + self::FULL => 'h:mm:ss a zzzz', + self::LONG => 'h:mm:ss a z', + self::MEDIUM => 'h:mm:ss a', + self::SHORT => 'h:mm a', + ); + + /** + * @var int + */ + private $datetype; + + /** + * @var int + */ + private $timetype; + + /** + * @var string + */ + private $pattern; + + /** + * @var \DateTimeZone + */ + private $dateTimeZone; + + /** + * @var Boolean + */ + private $unitializedTimeZoneId = false; + + /** + * @var string + */ + private $timeZoneId; + + /** + * Constructor + * + * @param string $locale The locale code + * @param int $datetype Type of date formatting, one of the format type constants + * @param int $timetype Type of time formatting, one of the format type constants + * @param string $timezone Timezone identifier + * @param int $calendar Calendar to use for formatting or parsing; default is Gregorian. + * One of the calendar constants. + * @param string $pattern Optional pattern to use when formatting + * + * @see http://www.php.net/manual/en/intldateformatter.create.php + * @see http://userguide.icu-project.org/formatparse/datetime + * + * @throws MethodArgumentValueNotImplementedException When $locale different than 'en' is passed + * @throws MethodArgumentValueNotImplementedException When $calendar different than GREGORIAN is passed + */ + public function __construct($locale, $datetype, $timetype, $timezone = null, $calendar = self::GREGORIAN, $pattern = null) + { + if ('en' !== $locale) { + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'locale', $locale, 'Only the \'en\' locale is supported'); + } + + if (self::GREGORIAN !== $calendar) { + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'calendar', $calendar, 'Only the GREGORIAN calendar is supported'); + } + + $this->datetype = $datetype; + $this->timetype = $timetype; + + $this->setPattern($pattern); + $this->setTimeZoneId($timezone); + } + + /** + * Static constructor + * + * @param string $locale The locale code + * @param int $datetype Type of date formatting, one of the format type constants + * @param int $timetype Type of time formatting, one of the format type constants + * @param string $timezone Timezone identifier + * @param int $calendar Calendar to use for formatting or parsing; default is Gregorian. + * One of the calendar constants. + * @param string $pattern Optional pattern to use when formatting + * + * @return StubIntlDateFormatter + * + * @see http://www.php.net/manual/en/intldateformatter.create.php + * @see http://userguide.icu-project.org/formatparse/datetime + * + * @throws MethodArgumentValueNotImplementedException When $locale different than 'en' is passed + */ + public static function create($locale, $datetype, $timetype, $timezone = null, $calendar = self::GREGORIAN, $pattern = null) + { + return new self($locale, $datetype, $timetype, $timezone, $calendar, $pattern); + } + + /** + * Format the date/time value (timestamp) as a string + * + * @param mixed $timestamp Unix timestamp to format + * + * @return string The formatted value + * + * @see http://www.php.net/manual/en/intldateformatter.format.php + * + * @throws MethodArgumentValueNotImplementedException If one of the formatting characters is not implemented + */ + public function format($timestamp) + { + // intl allows timestamps to be passed as arrays - we don't + if (is_array($timestamp)) { + $message = version_compare(PHP_VERSION, '5.3.4', '>=') ? + 'Only integer unix timestamps and DateTime objects are supported' : + 'Only integer unix timestamps are supported'; + + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'timestamp', $timestamp, $message); + } + + // behave like the intl extension + $argumentError = null; + if (version_compare(PHP_VERSION, '5.3.4', '<') && !is_int($timestamp)) { + $argumentError = 'datefmt_format: takes either an array or an integer timestamp value '; + } elseif (version_compare(PHP_VERSION, '5.3.4', '>=') && !is_int($timestamp) && !$timestamp instanceof \DateTime) { + $argumentError = 'datefmt_format: takes either an array or an integer timestamp value or a DateTime object'; + if (version_compare(PHP_VERSION, '5.5.0-dev', '>=') && !is_int($timestamp)) { + $argumentError = sprintf('datefmt_format: string \'%s\' is not numeric, which would be required for it to be a valid date', $timestamp); + } + } + + if (null !== $argumentError) { + StubIntlGlobals::setError(StubIntlGlobals::U_ILLEGAL_ARGUMENT_ERROR, $argumentError); + $this->errorCode = StubIntlGlobals::getErrorCode(); + $this->errorMessage = StubIntlGlobals::getErrorMessage(); + + return false; + } + + // As of PHP 5.3.4, IntlDateFormatter::format() accepts DateTime instances + if (version_compare(PHP_VERSION, '5.3.4', '>=') && $timestamp instanceof \DateTime) { + $timestamp = $timestamp->getTimestamp(); + } + + $transformer = new FullTransformer($this->getPattern(), $this->getTimeZoneId()); + $formatted = $transformer->format($this->createDateTime($timestamp)); + + // behave like the intl extension + StubIntlGlobals::setError(StubIntlGlobals::U_ZERO_ERROR); + $this->errorCode = StubIntlGlobals::getErrorCode(); + $this->errorMessage = StubIntlGlobals::getErrorMessage(); + + return $formatted; + } + + /** + * Formats an object + * + * @param object $object + * @param mixed $format + * @param string $locale + * + * @return string The formatted value + * + * @see http://www.php.net/manual/en/intldateformatter.formatobject.php + * + * @throws MethodNotImplementedException + */ + public function formatObject($object, $format = null, $locale = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the formatter's calendar + * + * @return int The calendar being used by the formatter + * + * @see http://www.php.net/manual/en/intldateformatter.getcalendar.php + */ + public function getCalendar() + { + return self::GREGORIAN; + } + + /** + * Returns the formatter's calendar object + * + * @return object The calendar's object being used by the formatter + * + * @see http://www.php.net/manual/en/intldateformatter.getcalendarobject.php + * + * @throws MethodNotImplementedException + */ + public function getCalendarObject() + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the formatter's datetype + * + * @return int The current value of the formatter + * + * @see http://www.php.net/manual/en/intldateformatter.getdatetype.php + */ + public function getDateType() + { + return $this->datetype; + } + + /** + * Returns formatter's last error code. Always returns the U_ZERO_ERROR class constant value + * + * @return int The error code from last formatter call + * + * @see http://www.php.net/manual/en/intldateformatter.geterrorcode.php + */ + public function getErrorCode() + { + return $this->errorCode; + } + + /** + * Returns formatter's last error message. Always returns the U_ZERO_ERROR_MESSAGE class constant value + * + * @return string The error message from last formatter call + * + * @see http://www.php.net/manual/en/intldateformatter.geterrormessage.php + */ + public function getErrorMessage() + { + return $this->errorMessage; + } + + /** + * Returns the formatter's locale + * + * @param int $type The locale name type to return between valid or actual (StubLocale::VALID_LOCALE or StubLocale::ACTUAL_LOCALE, respectively) + * + * @return string The locale name used to create the formatter + * + * @see http://www.php.net/manual/en/intldateformatter.getlocale.php + */ + public function getLocale($type = StubLocale::ACTUAL_LOCALE) + { + return 'en'; + } + + /** + * Returns the formatter's pattern + * + * @return string The pattern string used by the formatter + * + * @see http://www.php.net/manual/en/intldateformatter.getpattern.php + */ + public function getPattern() + { + return $this->pattern; + } + + /** + * Returns the formatter's time type + * + * @return string The time type used by the formatter + * + * @see http://www.php.net/manual/en/intldateformatter.gettimetype.php + */ + public function getTimeType() + { + return $this->timetype; + } + + /** + * Returns the formatter's timezone identifier + * + * @return string The timezone identifier used by the formatter + * + * @see http://www.php.net/manual/en/intldateformatter.gettimezoneid.php + */ + public function getTimeZoneId() + { + if (!$this->unitializedTimeZoneId) { + return $this->timeZoneId; + } + + // In PHP 5.5 default timezone depends on `date_default_timezone_get()` method + if (version_compare(PHP_VERSION, '5.5.0-dev', '>=')) { + return date_default_timezone_get(); + } + + return null; + } + + /** + * Returns the formatter's timezone + * + * @return mixed The timezone used by the formatter + * + * @see http://www.php.net/manual/en/intldateformatter.gettimezone.php + * + * @throws MethodNotImplementedException + */ + public function getTimeZone() + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns whether the formatter is lenient + * + * @return Boolean + * + * @see http://www.php.net/manual/en/intldateformatter.islenient.php + * + * @throws MethodNotImplementedException + */ + public function isLenient() + { + return false; + } + + /** + * Parse string to a field-based time value + * + * @param string $value String to convert to a time value + * @param int $position Position at which to start the parsing in $value (zero-based). + * If no error occurs before $value is consumed, $parse_pos will + * contain -1 otherwise it will contain the position at which parsing + * ended. If $parse_pos > strlen($value), the parse fails immediately. + * + * @return string Localtime compatible array of integers: contains 24 hour clock value in tm_hour field + * + * @see http://www.php.net/manual/en/intldateformatter.localtime.php + * + * @throws MethodNotImplementedException + */ + public function localtime($value, &$position = 0) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Parse string to a timestamp value + * + * @param string $value String to convert to a time value + * @param int $position Position at which to start the parsing in $value (zero-based). + * If no error occurs before $value is consumed, $parse_pos will + * contain -1 otherwise it will contain the position at which parsing + * ended. If $parse_pos > strlen($value), the parse fails immediately. + * + * @return string Parsed value as a timestamp + * + * @see http://www.php.net/manual/en/intldateformatter.parse.php + * + * @throws MethodArgumentNotImplementedException When $position different than null, behavior not implemented + */ + public function parse($value, &$position = null) + { + // We don't calculate the position when parsing the value + if (null !== $position) { + throw new MethodArgumentNotImplementedException(__METHOD__, 'position'); + } + + $dateTime = $this->createDateTime(0); + $transformer = new FullTransformer($this->getPattern(), $this->getTimeZoneId()); + + $timestamp = $transformer->parse($dateTime, $value); + + // behave like the intl extension. FullTransformer::parse() set the proper error + $this->errorCode = StubIntlGlobals::getErrorCode(); + $this->errorMessage = StubIntlGlobals::getErrorMessage(); + + return $timestamp; + } + + /** + * Set the formatter's calendar + * + * @param string $calendar The calendar to use. Default is IntlDateFormatter::GREGORIAN. + * + * @return Boolean true on success or false on failure + * + * @see http://www.php.net/manual/en/intldateformatter.setcalendar.php + * + * @throws MethodNotImplementedException + */ + public function setCalendar($calendar) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Set the leniency of the parser + * + * Define if the parser is strict or lenient in interpreting inputs that do not match the pattern + * exactly. Enabling lenient parsing allows the parser to accept otherwise flawed date or time + * patterns, parsing as much as possible to obtain a value. Extra space, unrecognized tokens, or + * invalid values ("February 30th") are not accepted. + * + * @param Boolean $lenient Sets whether the parser is lenient or not, default is false (strict) + * + * @return Boolean true on success or false on failure + * + * @see http://www.php.net/manual/en/intldateformatter.setlenient.php + * + * @throws MethodArgumentValueNotImplementedException When $lenient is true + */ + public function setLenient($lenient) + { + if ($lenient) { + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'lenient', $lenient, 'Only the strict parser is supported'); + } + } + + /** + * Set the formatter's pattern + * + * @param string $pattern A pattern string in conformance with the ICU IntlDateFormatter documentation + * + * @return Boolean true on success or false on failure + * + * @see http://www.php.net/manual/en/intldateformatter.setpattern.php + * @see http://userguide.icu-project.org/formatparse/datetime + */ + public function setPattern($pattern) + { + if (null === $pattern) { + $pattern = $this->getDefaultPattern(); + } + + $this->pattern = $pattern; + } + + /** + * Set the formatter's timezone identifier + * + * @param string $timeZoneId The time zone ID string of the time zone to use. + * If NULL or the empty string, the default time zone for the + * runtime is used. + * + * @return Boolean true on success or false on failure + * + * @see http://www.php.net/manual/en/intldateformatter.settimezoneid.php + */ + public function setTimeZoneId($timeZoneId) + { + if (null === $timeZoneId) { + // In PHP 5.5 if $timeZoneId is null it fallbacks to `date_default_timezone_get()` method + if (version_compare(PHP_VERSION, '5.5.0-dev', '>=')) { + $timeZoneId = date_default_timezone_get(); + } else { + // TODO: changes were made to ext/intl in PHP 5.4.4 release that need to be investigated since it will + // use ini's date.timezone when the time zone is not provided. As a not well tested workaround, uses UTC. + // See the first two items of the commit message for more information: + // https://github.com/php/php-src/commit/eb346ef0f419b90739aadfb6cc7b7436c5b521d9 + $timeZoneId = getenv('TZ') ?: 'UTC'; + } + + $this->unitializedTimeZoneId = true; + } + + // Backup original passed time zone + $timeZone = $timeZoneId; + + // Get an Etc/GMT time zone that is accepted for \DateTimeZone + if ('GMT' !== $timeZoneId && 0 === strpos($timeZoneId, 'GMT')) { + try { + $timeZoneId = DateFormat\TimeZoneTransformer::getEtcTimeZoneId($timeZoneId); + } catch (\InvalidArgumentException $e) { + // Does nothing, will fallback to UTC + } + } + + try { + $this->dateTimeZone = new \DateTimeZone($timeZoneId); + } catch (\Exception $e) { + $this->dateTimeZone = new \DateTimeZone('UTC'); + } + + $this->timeZoneId = $timeZone; + + return true; + } + + /** + * This method was added in PHP 5.5 as replacement for `setTimeZoneId()` + * + * @param mixed $timeZone + * + * @return Boolean true on success or false on failure + * + * @see http://www.php.net/manual/en/intldateformatter.settimezone.php + */ + public function setTimeZone($timeZone) + { + return $this->setTimeZoneId($timeZone); + } + + /** + * Create and returns a DateTime object with the specified timestamp and with the + * current time zone + * + * @param int $timestamp + * + * @return \DateTime + */ + protected function createDateTime($timestamp) + { + $dateTime = new \DateTime(); + $dateTime->setTimestamp($timestamp); + $dateTime->setTimezone($this->dateTimeZone); + + return $dateTime; + } + + /** + * Returns a pattern string based in the datetype and timetype values + * + * @return string + */ + protected function getDefaultPattern() + { + $patternParts = array(); + if (self::NONE !== $this->datetype) { + $patternParts[] = $this->defaultDateFormats[$this->datetype]; + } + if (self::NONE !== $this->timetype) { + $patternParts[] = $this->defaultTimeFormats[$this->timetype]; + } + $pattern = implode(' ', $patternParts); + + return $pattern; + } +} diff --git a/src/Symfony/Component/Intl/Exception/BadMethodCallException.php b/src/Symfony/Component/Intl/Exception/BadMethodCallException.php new file mode 100644 index 0000000000..ca79729f09 --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/BadMethodCallException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +/** + * Base BadMethodCallException for the Intl component. + * + * @author Bernhard Schussek + */ +class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Intl/Exception/ExceptionInterface.php b/src/Symfony/Component/Intl/Exception/ExceptionInterface.php new file mode 100644 index 0000000000..4fc076cafb --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +/** + * Base ExceptionInterface for the Intl component. + * + * @author Bernhard Schussek + */ +interface ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Intl/Exception/InvalidArgumentException.php b/src/Symfony/Component/Intl/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000..10f69ee34d --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +/** + * InvalidArgumentException for the Intl component. + * + * @author Bernhard Schussek + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Intl/Exception/MethodArgumentNotImplementedException.php b/src/Symfony/Component/Intl/Exception/MethodArgumentNotImplementedException.php new file mode 100644 index 0000000000..570609d0c3 --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/MethodArgumentNotImplementedException.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +use Symfony\Component\Intl\Exception\NotImplementedException; + +/** + * @author Eriksen Costa + */ +class MethodArgumentNotImplementedException extends NotImplementedException +{ + /** + * Constructor + * + * @param string $methodName The method name that raised the exception + * @param string $argName The argument name that is not implemented + */ + public function __construct($methodName, $argName) + { + $message = sprintf('The %s() method\'s argument $%s behavior is not implemented.', $methodName, $argName); + parent::__construct($message); + } +} diff --git a/src/Symfony/Component/Intl/Exception/MethodArgumentValueNotImplementedException.php b/src/Symfony/Component/Intl/Exception/MethodArgumentValueNotImplementedException.php new file mode 100644 index 0000000000..76e3f63424 --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/MethodArgumentValueNotImplementedException.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +use Symfony\Component\Intl\Exception\NotImplementedException; + +/** + * @author Eriksen Costa + */ +class MethodArgumentValueNotImplementedException extends NotImplementedException +{ + /** + * Constructor + * + * @param string $methodName The method name that raised the exception + * @param string $argName The argument name + * @param string $argValue The argument value that is not implemented + * @param string $additionalMessage An optional additional message to append to the exception message + */ + public function __construct($methodName, $argName, $argValue, $additionalMessage = '') + { + $message = sprintf( + 'The %s() method\'s argument $%s value %s behavior is not implemented.%s', + $methodName, + $argName, + var_export($argValue, true), + $additionalMessage !== '' ? ' '.$additionalMessage.'. ' : '' + ); + + parent::__construct($message); + } +} diff --git a/src/Symfony/Component/Intl/Exception/MethodNotImplementedException.php b/src/Symfony/Component/Intl/Exception/MethodNotImplementedException.php new file mode 100644 index 0000000000..d8a0e90f27 --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/MethodNotImplementedException.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +/** + * @author Eriksen Costa + */ +class MethodNotImplementedException extends NotImplementedException +{ + /** + * Constructor + * + * @param string $methodName The name of the method + */ + public function __construct($methodName) + { + parent::__construct(sprintf('The %s() is not implemented.', $methodName)); + } +} diff --git a/src/Symfony/Component/Intl/Exception/NotImplementedException.php b/src/Symfony/Component/Intl/Exception/NotImplementedException.php new file mode 100644 index 0000000000..1f3ba46b25 --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/NotImplementedException.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +/** + * Base exception class for not implemented behaviors of the intl extension in the Locale component. + * + * @author Eriksen Costa + */ +class NotImplementedException extends RuntimeException +{ + const INTL_INSTALL_MESSAGE = 'Please install the "intl" extension for full localization capabilities.'; + + /** + * Constructor + * + * @param string $message The exception message. A note to install the intl extension is appended to this string + */ + public function __construct($message) + { + parent::__construct($message.' '.self::INTL_INSTALL_MESSAGE); + } +} diff --git a/src/Symfony/Component/Intl/Exception/OutOfBoundsException.php b/src/Symfony/Component/Intl/Exception/OutOfBoundsException.php new file mode 100644 index 0000000000..2727141d2b --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/OutOfBoundsException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +/** + * Base OutOfBoundsException for the Intl component. + * + * @author Bernhard Schussek + */ +class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Intl/Exception/RuntimeException.php b/src/Symfony/Component/Intl/Exception/RuntimeException.php new file mode 100644 index 0000000000..989371427f --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +/** + * RuntimeException for the Intl component. + * + * @author Bernhard Schussek + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Intl/Globals/StubIntlGlobals.php b/src/Symfony/Component/Intl/Globals/StubIntlGlobals.php new file mode 100644 index 0000000000..405124b94d --- /dev/null +++ b/src/Symfony/Component/Intl/Globals/StubIntlGlobals.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Globals; + +/** + * Provides fake static versions of the global functions in the intl extension + * + * @author Bernhard Schussek + */ +abstract class StubIntlGlobals +{ + /** + * Indicates that no error occurred + * + * @var integer + */ + const U_ZERO_ERROR = 0; + + /** + * Indicates that an invalid argument was passed + * + * @var integer + */ + const U_ILLEGAL_ARGUMENT_ERROR = 1; + + /** + * Indicates that the parse() operation failed + * + * @var integer + */ + const U_PARSE_ERROR = 9; + + /** + * All known error codes + * + * @var array + */ + private static $errorCodes = array( + self::U_ZERO_ERROR => 'U_ZERO_ERROR', + self::U_ILLEGAL_ARGUMENT_ERROR => 'U_ILLEGAL_ARGUMENT_ERROR', + self::U_PARSE_ERROR => 'U_PARSE_ERROR', + ); + + /** + * The error code of the last operation + * + * @var integer + */ + private static $errorCode = self::U_ZERO_ERROR; + + /** + * The error code of the last operation + * + * @var integer + */ + private static $errorMessage = 'U_ZERO_ERROR'; + + /** + * Returns whether the error code indicates a failure + * + * @param integer $errorCode The error code returned by StubIntlGlobals::getErrorCode() + * + * @return Boolean + */ + public static function isFailure($errorCode) + { + return isset(self::$errorCodes[$errorCode]) + && $errorCode > self::U_ZERO_ERROR; + } + + /** + * Returns the error code of the last operation + * + * Returns StubIntlGlobals::U_ZERO_ERROR if no error occurred. + * + * @return integer + */ + public static function getErrorCode() + { + return self::$errorCode; + } + + /** + * Returns the error message of the last operation + * + * Returns "U_ZERO_ERROR" if no error occurred. + * + * @return string + */ + public static function getErrorMessage() + { + return self::$errorMessage; + } + + /** + * Returns the symbolic name for a given error code + * + * @param integer $code The error code returned by StubIntlGlobals::getErrorCode() + * + * @return string + */ + public static function getErrorName($code) + { + if (isset(self::$errorCodes[$code])) { + return self::$errorCodes[$code]; + } + + return '[BOGUS UErrorCode]'; + } + + /** + * Sets the current error + * + * @param integer $code One of the error constants in this class + * @param string $message The ICU class error message + * + * @throws \InvalidArgumentException If the code is not one of the error constants in this class + */ + public static function setError($code, $message = '') + { + if (!isset(self::$errorCodes[$code])) { + throw new \InvalidArgumentException(sprintf('No such error code: "%s"', $code)); + } + + self::$errorMessage = $message ? sprintf('%s: %s', $message, self::$errorCodes[$code]) : self::$errorCodes[$code]; + self::$errorCode = $code; + } +} diff --git a/src/Symfony/Component/Intl/Intl.php b/src/Symfony/Component/Intl/Intl.php new file mode 100644 index 0000000000..706bd8dab1 --- /dev/null +++ b/src/Symfony/Component/Intl/Intl.php @@ -0,0 +1,332 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl; + +use Symfony\Component\Icu\IcuCurrencyBundle; +use Symfony\Component\Icu\IcuData; +use Symfony\Component\Icu\IcuLanguageBundle; +use Symfony\Component\Icu\IcuLocaleBundle; +use Symfony\Component\Icu\IcuRegionBundle; +use Symfony\Component\Intl\Exception\InvalidArgumentException; +use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader; +use Symfony\Component\Intl\ResourceBundle\Reader\BufferedReader; +use Symfony\Component\Intl\ResourceBundle\Reader\PhpBundleReader; +use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReader; +use Symfony\Component\Intl\ResourceBundle\Stub\StubCurrencyBundle; +use Symfony\Component\Intl\ResourceBundle\Stub\StubLanguageBundle; +use Symfony\Component\Intl\ResourceBundle\Stub\StubLocaleBundle; +use Symfony\Component\Intl\ResourceBundle\Stub\StubRegionBundle; + +/** + * Gives access to internationalization data. + * + * @author Bernhard Schussek + */ +class Intl +{ + /** + * Load data from the Icu component. + */ + const ICU = 0; + + /** + * Load data from the stub files of the Intl component. + */ + const STUB = 1; + + /** + * The number of resource bundles to buffer. Loading the same resource + * bundle for n locales takes up n spots in the buffer. + */ + const BUFFER_SIZE = 10; + + /** + * The accepted values for the {@link $dataSource} property. + * + * @var array + */ + private static $allowedDataSources = array( + self::ICU => 'Intl::ICU', + self::STUB => 'Intl::STUB', + ); + + /** + * @var integer + */ + private static $dataSource; + + /** + * @var ResourceBundle\CurrencyBundleInterface + */ + private static $currencyBundle; + + /** + * @var ResourceBundle\LanguageBundleInterface + */ + private static $languageBundle; + + /** + * @var ResourceBundle\LocaleBundleInterface + */ + private static $localeBundle; + + /** + * @var ResourceBundle\RegionBundleInterface + */ + private static $regionBundle; + + /** + * @var string|Boolean|null + */ + private static $icuVersion = false; + + /** + * @var string + */ + private static $icuDataVersion = false; + + /** + * @var ResourceBundle\Reader\StructuredBundleReaderInterface + */ + private static $phpReader; + + /** + * @var ResourceBundle\Reader\StructuredBundleReaderInterface + */ + private static $binaryReader; + + /** + * Returns whether the intl extension is installed. + * + * @return Boolean Returns true if the intl extension is installed, false otherwise. + */ + public static function isExtensionLoaded() + { + return IcuData::isLoadable(); + } + + /** + * Sets the data source from which to load the resource bundles. + * + * @param integer $dataSource One of the constants {@link Intl::ICU} or + * {@link Intl::STUB}. + * + * @throws InvalidArgumentException If the data source is invalid. + * + * @see getData>Source + */ + public static function setDataSource($dataSource) + { + if (!isset(self::$allowedDataSources[$dataSource])) { + throw new InvalidArgumentException(sprintf( + 'The data sources should be one of %s', + implode(', ', self::$allowedDataSources) + )); + } + + if (self::ICU === $dataSource && !IcuData::isLoadable()) { + throw new InvalidArgumentException( + 'The data source cannot be set to Intl::ICU if the intl ' . + 'extension is not installed.' + ); + } + + if ($dataSource !== self::$dataSource) { + self::$currencyBundle = null; + self::$languageBundle = null; + self::$localeBundle = null; + self::$regionBundle = null; + } + + self::$dataSource = $dataSource; + } + + /** + * Returns the data source from which to load the resource bundles. + * + * If {@link setDataSource()} has not been called, the data source will be + * chosen depending on whether the intl extension is installed or not: + * + * * If the extension is present, the bundles will be loaded from the Icu + * component; + * * Otherwise, the bundles will be loaded from the stub files in the + * Intl component. + * + * @return integer One of the constants {@link Intl::ICU} or + * {@link Intl::STUB}. + */ + public static function getDataSource() + { + if (null === self::$dataSource) { + self::$dataSource = IcuData::isLoadable() ? self::ICU : self::STUB; + } + + return self::$dataSource; + } + + /** + * Returns the bundle containing currency information. + * + * @return ResourceBundle\CurrencyBundleInterface The currency resource bundle. + */ + public static function getCurrencyBundle() + { + if (null === self::$currencyBundle) { + self::$currencyBundle = self::ICU === self::getDataSource() + ? new IcuCurrencyBundle(self::getBinaryReader()) + : new StubCurrencyBundle(self::getPhpReader()); + } + + return self::$currencyBundle; + } + + /** + * Returns the bundle containing language information. + * + * @return ResourceBundle\LanguageBundleInterface The language resource bundle. + */ + public static function getLanguageBundle() + { + if (null === self::$languageBundle) { + self::$languageBundle = self::ICU === self::getDataSource() + ? new IcuLanguageBundle(self::getBinaryReader()) + : new StubLanguageBundle(self::getPhpReader()); + } + + return self::$languageBundle; + } + + /** + * Returns the bundle containing locale information. + * + * @return ResourceBundle\LocaleBundleInterface The locale resource bundle. + */ + public static function getLocaleBundle() + { + if (null === self::$localeBundle) { + self::$localeBundle = self::ICU === self::getDataSource() + ? new IcuLocaleBundle(self::getBinaryReader()) + : new StubLocaleBundle(self::getPhpReader()); + } + + return self::$localeBundle; + } + + /** + * Returns the bundle containing region information. + * + * @return ResourceBundle\RegionBundleInterface The region resource bundle. + */ + public static function getRegionBundle() + { + if (null === self::$regionBundle) { + self::$regionBundle = self::ICU === self::getDataSource() + ? new IcuRegionBundle(self::getBinaryReader()) + : new StubRegionBundle(self::getPhpReader()); + } + + return self::$regionBundle; + } + + /** + * Returns the version of the installed ICU library. + * + * @return null|string The ICU version or NULL if it could not be determined. + */ + public static function getIcuVersion() + { + if (false === self::$icuVersion) { + if (defined('INTL_ICU_VERSION')) { + self::$icuVersion = INTL_ICU_VERSION; + } else { + try { + $reflector = new \ReflectionExtension('intl'); + ob_start(); + $reflector->info(); + $output = strip_tags(ob_get_clean()); + preg_match('/^ICU version (?:=>)?(.*)$/m', $output, $matches); + + self::$icuVersion = trim($matches[1]); + } catch (\ReflectionException $e) { + self::$icuVersion = null; + } + } + } + + return self::$icuVersion; + } + + /** + * Returns the version of the installed ICU data. + * + * @return string The version of the installed ICU data. + */ + public static function getIcuDataVersion() + { + if (false === self::$icuDataVersion) { + self::$icuDataVersion = self::ICU === self::getDataSource() + ? IcuData::getVersion() + : file_get_contents(__DIR__ . '/Resources/version.txt'); + } + + return self::$icuDataVersion; + } + + /** + * Returns the ICU version that the stub classes mimic. + * + * @return string The ICU version of the stub classes. + */ + public static function getStubIcuVersion() + { + return '50.1.0'; + } + + /** + * Returns a resource bundle reader for .php resource bundle files. + * + * @return ResourceBundle\Reader\StructuredBundleReaderInterface The resource reader. + */ + private static function getPhpReader() + { + if (null === self::$phpReader) { + self::$phpReader = new StructuredBundleReader(new BufferedReader( + new PhpBundleReader(), + self::BUFFER_SIZE + )); + } + + return self::$phpReader; + } + + /** + * Returns a resource bundle reader for binary .res resource bundle files. + * + * @return ResourceBundle\Reader\StructuredBundleReaderInterface The resource reader. + */ + private static function getBinaryReader() + { + if (null === self::$binaryReader) { + self::$binaryReader = new StructuredBundleReader(new BufferedReader( + new BinaryBundleReader(), + self::BUFFER_SIZE + )); + } + + return self::$binaryReader; + } + + /** + * This class must not be instantiated. + */ + private function __construct() {} +} diff --git a/src/Symfony/Component/Intl/LICENSE b/src/Symfony/Component/Intl/LICENSE new file mode 100644 index 0000000000..88a57f8d8d --- /dev/null +++ b/src/Symfony/Component/Intl/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2013 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Intl/Locale/StubLocale.php b/src/Symfony/Component/Intl/Locale/StubLocale.php new file mode 100644 index 0000000000..5b940ec719 --- /dev/null +++ b/src/Symfony/Component/Intl/Locale/StubLocale.php @@ -0,0 +1,317 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Locale; + +use Symfony\Component\Intl\Locale; +use Symfony\Component\Intl\Exception\NotImplementedException; +use Symfony\Component\Intl\Exception\MethodNotImplementedException; + +/** + * Provides a stub Locale for the 'en' locale. + * + * @author Eriksen Costa + */ +class StubLocale +{ + const DEFAULT_LOCALE = null; + + /** Locale method constants */ + const ACTUAL_LOCALE = 0; + const VALID_LOCALE = 1; + + /** Language tags constants */ + const LANG_TAG = 'language'; + const EXTLANG_TAG = 'extlang'; + const SCRIPT_TAG = 'script'; + const REGION_TAG = 'region'; + const VARIANT_TAG = 'variant'; + const GRANDFATHERED_LANG_TAG = 'grandfathered'; + const PRIVATE_TAG = 'private'; + + /** + * Returns the best available locale based on HTTP "Accept-Language" header according to RFC 2616 + * + * @param string $header The string containing the "Accept-Language" header value + * + * @return string The corresponding locale code + * + * @see http://www.php.net/manual/en/locale.acceptfromhttp.php + * + * @throws MethodNotImplementedException + */ + public static function acceptFromHttp($header) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns a correctly ordered and delimited locale code + * + * @param array $subtags A keyed array where the keys identify the particular locale code subtag + * + * @return string The corresponding locale code + * + * @see http://www.php.net/manual/en/locale.composelocale.php + * + * @throws MethodNotImplementedException + */ + public static function composeLocale(array $subtags) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Checks if a language tag filter matches with locale + * + * @param string $langtag The language tag to check + * @param string $locale The language range to check against + * @param Boolean $canonicalize + * + * @return string The corresponding locale code + * + * @see http://www.php.net/manual/en/locale.filtermatches.php + * + * @throws MethodNotImplementedException + */ + public static function filterMatches($langtag, $locale, $canonicalize = false) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the variants for the input locale + * + * @param string $locale The locale to extract the variants from + * + * @return array The locale variants + * + * @see http://www.php.net/manual/en/locale.getallvariants.php + * + * @throws MethodNotImplementedException + */ + public static function getAllVariants($locale) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the default locale + * + * @return string The default locale code. Always returns 'en' + * + * @see http://www.php.net/manual/en/locale.getdefault.php + * + * @throws MethodNotImplementedException + */ + public static function getDefault() + { + return 'en'; + } + + /** + * Returns the localized display name for the locale language + * + * @param string $locale The locale code to return the display language from + * @param string $inLocale Optional format locale code to use to display the language name + * + * @return string The localized language display name + * + * @see http://www.php.net/manual/en/locale.getdisplaylanguage.php + * + * @throws MethodNotImplementedException + */ + public static function getDisplayLanguage($locale, $inLocale = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the localized display name for the locale + * + * @param string $locale The locale code to return the display locale name from + * @param string $inLocale Optional format locale code to use to display the locale name + * + * @return string The localized locale display name + * + * @see http://www.php.net/manual/en/locale.getdisplayname.php + * + * @throws MethodNotImplementedException + */ + public static function getDisplayName($locale, $inLocale = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the localized display name for the locale region + * + * @param string $locale The locale code to return the display region from + * @param string $inLocale Optional format locale code to use to display the region name + * + * @return string The localized region display name + * + * @see http://www.php.net/manual/en/locale.getdisplayregion.php + * + * @throws MethodNotImplementedException + */ + public static function getDisplayRegion($locale, $inLocale = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the localized display name for the locale script + * + * @param string $locale The locale code to return the display script from + * @param string $inLocale Optional format locale code to use to display the script name + * + * @return string The localized script display name + * + * @see http://www.php.net/manual/en/locale.getdisplayscript.php + * + * @throws MethodNotImplementedException + */ + public static function getDisplayScript($locale, $inLocale = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the localized display name for the locale variant + * + * @param string $locale The locale code to return the display variant from + * @param string $inLocale Optional format locale code to use to display the variant name + * + * @return string The localized variant display name + * + * @see http://www.php.net/manual/en/locale.getdisplayvariant.php + * + * @throws MethodNotImplementedException + */ + public static function getDisplayVariant($locale, $inLocale = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the keywords for the locale + * + * @param string $locale The locale code to extract the keywords from + * + * @return array Associative array with the extracted variants + * + * @see http://www.php.net/manual/en/locale.getkeywords.php + * + * @throws MethodNotImplementedException + */ + public static function getKeywords($locale) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the primary language for the locale + * + * @param string $locale The locale code to extract the language code from + * + * @return string|null The extracted language code or null in case of error + * + * @see http://www.php.net/manual/en/locale.getprimarylanguage.php + * + * @throws MethodNotImplementedException + */ + public static function getPrimaryLanguage($locale) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the region for the locale + * + * @param string $locale The locale code to extract the region code from + * + * @return string|null The extracted region code or null if not present + * + * @see http://www.php.net/manual/en/locale.getregion.php + * + * @throws MethodNotImplementedException + */ + public static function getRegion($locale) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the script for the locale + * + * @param string $locale The locale code to extract the script code from + * + * @return string|null The extracted script code or null if not present + * + * @see http://www.php.net/manual/en/locale.getscript.php + * + * @throws MethodNotImplementedException + */ + public static function getScript($locale) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the closest language tag for the locale + * + * @param array $langtag A list of the language tags to compare to locale + * @param string $locale The locale to use as the language range when matching + * @param Boolean $canonicalize If true, the arguments will be converted to canonical form before matching + * @param string $default The locale to use if no match is found + * + * @see http://www.php.net/manual/en/locale.lookup.php + * + * @throws MethodNotImplementedException + */ + public static function lookup(array $langtag, $locale, $canonicalize = false, $default = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns an associative array of locale identifier subtags + * + * @param string $locale The locale code to extract the subtag array from + * + * @return array Associative array with the extracted subtags + * + * @see http://www.php.net/manual/en/locale.parselocale.php + * + * @throws MethodNotImplementedException + */ + public static function parseLocale($locale) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Sets the default runtime locale + * + * @param string $locale The locale code + * + * @return Boolean true on success or false on failure + * + * @see http://www.php.net/manual/en/locale.parselocale.php + * + * @throws MethodNotImplementedException + */ + public static function setDefault($locale) + { + throw new MethodNotImplementedException(__METHOD__); + } +} diff --git a/src/Symfony/Component/Intl/NumberFormatter/StubNumberFormatter.php b/src/Symfony/Component/Intl/NumberFormatter/StubNumberFormatter.php new file mode 100644 index 0000000000..093508978d --- /dev/null +++ b/src/Symfony/Component/Intl/NumberFormatter/StubNumberFormatter.php @@ -0,0 +1,869 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\NumberFormatter; + +use Symfony\Component\Intl\Exception\NotImplementedException; +use Symfony\Component\Intl\Exception\MethodNotImplementedException; +use Symfony\Component\Intl\Exception\MethodArgumentNotImplementedException; +use Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException; +use Symfony\Component\Intl\Globals\StubIntlGlobals; +use Symfony\Component\Intl\Intl; +use Symfony\Component\Intl\Locale\StubLocale; + +/** + * Provides a stub NumberFormatter for the 'en' locale. + * + * @author Eriksen Costa + */ +class StubNumberFormatter +{ + /** Format style constants */ + const PATTERN_DECIMAL = 0; + const DECIMAL = 1; + const CURRENCY = 2; + const PERCENT = 3; + const SCIENTIFIC = 4; + const SPELLOUT = 5; + const ORDINAL = 6; + const DURATION = 7; + const PATTERN_RULEBASED = 9; + const IGNORE = 0; + const DEFAULT_STYLE = 1; + + /** Format type constants */ + const TYPE_DEFAULT = 0; + const TYPE_INT32 = 1; + const TYPE_INT64 = 2; + const TYPE_DOUBLE = 3; + const TYPE_CURRENCY = 4; + + /** Numeric attribute constants */ + const PARSE_INT_ONLY = 0; + const GROUPING_USED = 1; + const DECIMAL_ALWAYS_SHOWN = 2; + const MAX_INTEGER_DIGITS = 3; + const MIN_INTEGER_DIGITS = 4; + const INTEGER_DIGITS = 5; + const MAX_FRACTION_DIGITS = 6; + const MIN_FRACTION_DIGITS = 7; + const FRACTION_DIGITS = 8; + const MULTIPLIER = 9; + const GROUPING_SIZE = 10; + const ROUNDING_MODE = 11; + const ROUNDING_INCREMENT = 12; + const FORMAT_WIDTH = 13; + const PADDING_POSITION = 14; + const SECONDARY_GROUPING_SIZE = 15; + const SIGNIFICANT_DIGITS_USED = 16; + const MIN_SIGNIFICANT_DIGITS = 17; + const MAX_SIGNIFICANT_DIGITS = 18; + const LENIENT_PARSE = 19; + + /** Text attribute constants */ + const POSITIVE_PREFIX = 0; + const POSITIVE_SUFFIX = 1; + const NEGATIVE_PREFIX = 2; + const NEGATIVE_SUFFIX = 3; + const PADDING_CHARACTER = 4; + const CURRENCY_CODE = 5; + const DEFAULT_RULESET = 6; + const PUBLIC_RULESETS = 7; + + /** Format symbol constants */ + const DECIMAL_SEPARATOR_SYMBOL = 0; + const GROUPING_SEPARATOR_SYMBOL = 1; + const PATTERN_SEPARATOR_SYMBOL = 2; + const PERCENT_SYMBOL = 3; + const ZERO_DIGIT_SYMBOL = 4; + const DIGIT_SYMBOL = 5; + const MINUS_SIGN_SYMBOL = 6; + const PLUS_SIGN_SYMBOL = 7; + const CURRENCY_SYMBOL = 8; + const INTL_CURRENCY_SYMBOL = 9; + const MONETARY_SEPARATOR_SYMBOL = 10; + const EXPONENTIAL_SYMBOL = 11; + const PERMILL_SYMBOL = 12; + const PAD_ESCAPE_SYMBOL = 13; + const INFINITY_SYMBOL = 14; + const NAN_SYMBOL = 15; + const SIGNIFICANT_DIGIT_SYMBOL = 16; + const MONETARY_GROUPING_SEPARATOR_SYMBOL = 17; + + /** Rounding mode values used by NumberFormatter::setAttribute() with NumberFormatter::ROUNDING_MODE attribute */ + const ROUND_CEILING = 0; + const ROUND_FLOOR = 1; + const ROUND_DOWN = 2; + const ROUND_UP = 3; + const ROUND_HALFEVEN = 4; + const ROUND_HALFDOWN = 5; + const ROUND_HALFUP = 6; + + /** Pad position values used by NumberFormatter::setAttribute() with NumberFormatter::PADDING_POSITION attribute */ + const PAD_BEFORE_PREFIX = 0; + const PAD_AFTER_PREFIX = 1; + const PAD_BEFORE_SUFFIX = 2; + const PAD_AFTER_SUFFIX = 3; + + /** + * The error code from the last operation + * + * @var integer + */ + protected $errorCode = StubIntlGlobals::U_ZERO_ERROR; + + /** + * The error message from the last operation + * + * @var string + */ + protected $errorMessage = 'U_ZERO_ERROR'; + + /** + * @var string + */ + private $locale; + + /** + * @var int + */ + private $style; + + /** + * Default values for the en locale + * + * @var array + */ + private $attributes = array( + self::FRACTION_DIGITS => 0, + self::GROUPING_USED => 1, + self::ROUNDING_MODE => self::ROUND_HALFEVEN + ); + + /** + * Holds the initialized attributes code + * + * @var array + */ + private $initializedAttributes = array(); + + /** + * The supported styles to the constructor $styles argument + * + * @var array + */ + private static $supportedStyles = array( + 'CURRENCY' => self::CURRENCY, + 'DECIMAL' => self::DECIMAL + ); + + /** + * Supported attributes to the setAttribute() $attr argument + * + * @var array + */ + private static $supportedAttributes = array( + 'FRACTION_DIGITS' => self::FRACTION_DIGITS, + 'GROUPING_USED' => self::GROUPING_USED, + 'ROUNDING_MODE' => self::ROUNDING_MODE + ); + + /** + * The available rounding modes for setAttribute() usage with + * StubNumberFormatter::ROUNDING_MODE. StubNumberFormatter::ROUND_DOWN + * and StubNumberFormatter::ROUND_UP does not have a PHP only equivalent + * + * @var array + */ + private static $roundingModes = array( + 'ROUND_HALFEVEN' => self::ROUND_HALFEVEN, + 'ROUND_HALFDOWN' => self::ROUND_HALFDOWN, + 'ROUND_HALFUP' => self::ROUND_HALFUP + ); + + /** + * The mapping between NumberFormatter rounding modes to the available + * modes in PHP's round() function. + * + * @see http://www.php.net/manual/en/function.round.php + * + * @var array + */ + private static $phpRoundingMap = array( + self::ROUND_HALFDOWN => \PHP_ROUND_HALF_DOWN, + self::ROUND_HALFEVEN => \PHP_ROUND_HALF_EVEN, + self::ROUND_HALFUP => \PHP_ROUND_HALF_UP + ); + + /** + * The maximum values of the integer type in 32 bit platforms. + * + * @var array + */ + private static $int32Range = array( + 'positive' => 2147483647, + 'negative' => -2147483648 + ); + + /** + * The maximum values of the integer type in 64 bit platforms. + * + * @var array + */ + private static $int64Range = array( + 'positive' => 9223372036854775807, + 'negative' => -9223372036854775808 + ); + + /** + * Constructor + * + * @param string $locale The locale code + * @param int $style Style of the formatting, one of the format style constants + * @param string $pattern A pattern string in case $style is NumberFormat::PATTERN_DECIMAL or + * NumberFormat::PATTERN_RULEBASED. It must conform to the syntax + * described in the ICU DecimalFormat or ICU RuleBasedNumberFormat documentation + * + * @see http://www.php.net/manual/en/numberformatter.create.php + * @see http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details + * @see http://www.icu-project.org/apiref/icu4c/classRuleBasedNumberFormat.html#_details + * + * @throws MethodArgumentValueNotImplementedException When $locale different than 'en' is passed + * @throws MethodArgumentValueNotImplementedException When the $style is not supported + * @throws MethodArgumentNotImplementedException When the pattern value is different than null + */ + public function __construct($locale = 'en', $style = null, $pattern = null) + { + if ('en' != $locale) { + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'locale', $locale, 'Only the \'en\' locale is supported'); + } + + if (!in_array($style, self::$supportedStyles)) { + $message = sprintf('The available styles are: %s.', implode(', ', array_keys(self::$supportedStyles))); + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'style', $style, $message); + } + + if (null !== $pattern) { + throw new MethodArgumentNotImplementedException(__METHOD__, 'pattern'); + } + + $this->locale = $locale; + $this->style = $style; + } + + /** + * Static constructor + * + * @param string $locale The locale code + * @param int $style Style of the formatting, one of the format style constants + * @param string $pattern A pattern string in case $style is NumberFormat::PATTERN_DECIMAL or + * NumberFormat::PATTERN_RULEBASED. It must conform to the syntax + * described in the ICU DecimalFormat or ICU RuleBasedNumberFormat documentation + * + * @return StubNumberFormatter + * + * @see http://www.php.net/manual/en/numberformatter.create.php + * @see http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details + * @see http://www.icu-project.org/apiref/icu4c/classRuleBasedNumberFormat.html#_details + * + * @throws MethodArgumentValueNotImplementedException When $locale different than 'en' is passed + * @throws MethodArgumentValueNotImplementedException When the $style is not supported + * @throws MethodArgumentNotImplementedException When the pattern value is different than null + */ + public static function create($locale = 'en', $style = null, $pattern = null) + { + return new self($locale, $style, $pattern); + } + + /** + * Format a currency value + * + * @param float $value The numeric currency value + * @param string $currency The 3-letter ISO 4217 currency code indicating the currency to use + * + * @return string The formatted currency value + * + * @see http://www.php.net/manual/en/numberformatter.formatcurrency.php + * @see http://www.iso.org/iso/support/faqs/faqs_widely_used_standards/widely_used_standards_other/currency_codes/currency_codes_list-1.htm + */ + public function formatCurrency($value, $currency) + { + if ($this->style == self::DECIMAL) { + return $this->format($value); + } + + $symbol = Intl::getCurrencyBundle()->getCurrencySymbol('en', $currency); + $fractionDigits = Intl::getCurrencyBundle()->getFractionDigits($currency); + + $value = $this->roundCurrency($value, $currency); + + $negative = false; + if (0 > $value) { + $negative = true; + $value *= -1; + } + + $value = $this->formatNumber($value, $fractionDigits); + + $ret = $symbol.$value; + + return $negative ? '('.$ret.')' : $ret; + } + + /** + * Format a number + * + * @param number $value The value to format + * @param int $type Type of the formatting, one of the format type constants + * + * @return Boolean|string The formatted value or false on error + * + * @see http://www.php.net/manual/en/numberformatter.format.php + * + * @throws \RuntimeException If the method is called with the class $style 'CURRENCY' + * @throws MethodArgumentNotImplementedException If the $type is different than TYPE_DEFAULT + */ + public function format($value, $type = self::TYPE_DEFAULT) + { + // The original NumberFormatter does not support this format type + if ($type == self::TYPE_CURRENCY) { + trigger_error(__METHOD__.'(): Unsupported format type '.$type, \E_USER_WARNING); + + return false; + } + + if ($this->style == self::CURRENCY) { + throw new NotImplementedException(sprintf( + '%s() method does not support the formatting of currencies (instance with CURRENCY style). %s', + __METHOD__, NotImplementedException::INTL_INSTALL_MESSAGE + )); + } + + // Only the default type is supported. + if ($type != self::TYPE_DEFAULT) { + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'type', $type, 'Only TYPE_DEFAULT is supported'); + } + + $fractionDigits = $this->getAttribute(self::FRACTION_DIGITS); + + $value = $this->round($value, $fractionDigits); + $value = $this->formatNumber($value, $fractionDigits); + + // behave like the intl extension + $this->resetError(); + + return $value; + } + + /** + * Returns an attribute value + * + * @param int $attr An attribute specifier, one of the numeric attribute constants + * + * @return Boolean|int The attribute value on success or false on error + * + * @see http://www.php.net/manual/en/numberformatter.getattribute.php + */ + public function getAttribute($attr) + { + return isset($this->attributes[$attr]) ? $this->attributes[$attr] : null; + } + + /** + * Returns formatter's last error code. Always returns the U_ZERO_ERROR class constant value + * + * @return int The error code from last formatter call + * + * @see http://www.php.net/manual/en/numberformatter.geterrorcode.php + */ + public function getErrorCode() + { + return $this->errorCode; + } + + /** + * Returns formatter's last error message. Always returns the U_ZERO_ERROR_MESSAGE class constant value + * + * @return string The error message from last formatter call + * + * @see http://www.php.net/manual/en/numberformatter.geterrormessage.php + */ + public function getErrorMessage() + { + return $this->errorMessage; + } + + /** + * Returns the formatter's locale + * + * @param int $type The locale name type to return between valid or actual (StubLocale::VALID_LOCALE or StubLocale::ACTUAL_LOCALE, respectively) + * + * @return string The locale name used to create the formatter + * + * @see http://www.php.net/manual/en/numberformatter.getlocale.php + */ + public function getLocale($type = StubLocale::ACTUAL_LOCALE) + { + return $this->locale; + } + + /** + * Returns the formatter's pattern + * + * @return Boolean|string The pattern string used by the formatter or false on error + * + * @see http://www.php.net/manual/en/numberformatter.getpattern.php + * + * @throws MethodNotImplementedException + */ + public function getPattern() + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns a formatter symbol value + * + * @param int $attr A symbol specifier, one of the format symbol constants + * + * @return Boolean|string The symbol value or false on error + * + * @see http://www.php.net/manual/en/numberformatter.getsymbol.php + * + * @throws MethodNotImplementedException + */ + public function getSymbol($attr) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns a formatter text attribute value + * + * @param int $attr An attribute specifier, one of the text attribute constants + * + * @return Boolean|string The attribute value or false on error + * + * @see http://www.php.net/manual/en/numberformatter.gettextattribute.php + * + * @throws MethodNotImplementedException + */ + public function getTextAttribute($attr) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Parse a currency number + * + * @param string $value The value to parse + * @param string $currency Parameter to receive the currency name (reference) + * @param int $position Offset to begin the parsing on return this value will hold the offset at which the parsing ended + * + * @return Boolean|string The parsed numeric value of false on error + * + * @see http://www.php.net/manual/en/numberformatter.parsecurrency.php + * + * @throws MethodNotImplementedException + */ + public function parseCurrency($value, &$currency, &$position = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Parse a number + * + * @param string $value The value to parse + * @param string $type Type of the formatting, one of the format type constants. NumberFormatter::TYPE_DOUBLE by default + * @param int $position Offset to begin the parsing on return this value will hold the offset at which the parsing ended + * + * @return Boolean|string The parsed value of false on error + * + * @see http://www.php.net/manual/en/numberformatter.parse.php + * + * @throws MethodArgumentNotImplementedException When $position different than null, behavior not implemented + */ + public function parse($value, $type = self::TYPE_DOUBLE, &$position = null) + { + if ($type == self::TYPE_DEFAULT || $type == self::TYPE_CURRENCY) { + trigger_error(__METHOD__.'(): Unsupported format type '.$type, \E_USER_WARNING); + + return false; + } + + // We don't calculate the position when parsing the value + if (null !== $position) { + throw new MethodArgumentNotImplementedException(__METHOD__, 'position'); + } + + preg_match('/^([^0-9\-]{0,})(.*)/', $value, $matches); + + // Any string before the numeric value causes error in the parsing + if (isset($matches[1]) && !empty($matches[1])) { + StubIntlGlobals::setError(StubIntlGlobals::U_PARSE_ERROR, 'Number parsing failed'); + $this->errorCode = StubIntlGlobals::getErrorCode(); + $this->errorMessage = StubIntlGlobals::getErrorMessage(); + + return false; + } + + // Remove everything that is not number or dot (.) + $value = preg_replace('/[^0-9\.\-]/', '', $value); + $value = $this->convertValueDataType($value, $type); + + // behave like the intl extension + $this->resetError(); + + return $value; + } + + /** + * Set an attribute + * + * @param int $attr An attribute specifier, one of the numeric attribute constants + * @param int $value The attribute value + * + * @return Boolean true on success or false on failure + * + * @see http://www.php.net/manual/en/numberformatter.setattribute.php + * + * @throws MethodArgumentValueNotImplementedException When the $attr is not supported + * @throws MethodArgumentValueNotImplementedException When the $value is not supported + */ + public function setAttribute($attr, $value) + { + if (!in_array($attr, self::$supportedAttributes)) { + $message = sprintf( + 'The available attributes are: %s', + implode(', ', array_keys(self::$supportedAttributes)) + ); + + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'attr', $value, $message); + } + + if (self::$supportedAttributes['ROUNDING_MODE'] == $attr && $this->isInvalidRoundingMode($value)) { + $message = sprintf( + 'The supported values for ROUNDING_MODE are: %s', + implode(', ', array_keys(self::$roundingModes)) + ); + + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'attr', $value, $message); + } + + if (self::$supportedAttributes['GROUPING_USED'] == $attr) { + $value = $this->normalizeGroupingUsedValue($value); + } + + if (self::$supportedAttributes['FRACTION_DIGITS'] == $attr) { + $value = $this->normalizeFractionDigitsValue($value); + } + + $this->attributes[$attr] = $value; + $this->initializedAttributes[$attr] = true; + + return true; + } + + /** + * Set the formatter's pattern + * + * @param string $pattern A pattern string in conformance with the ICU DecimalFormat documentation + * + * @return Boolean true on success or false on failure + * + * @see http://www.php.net/manual/en/numberformatter.setpattern.php + * @see http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details + * + * @throws MethodNotImplementedException + */ + public function setPattern($pattern) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Set the formatter's symbol + * + * @param int $attr A symbol specifier, one of the format symbol constants + * @param string $value The value for the symbol + * + * @return Boolean true on success or false on failure + * + * @see http://www.php.net/manual/en/numberformatter.setsymbol.php + * + * @throws MethodNotImplementedException + */ + public function setSymbol($attr, $value) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Set a text attribute + * + * @param int $attr An attribute specifier, one of the text attribute constants + * @param int $value The attribute value + * + * @return Boolean true on success or false on failure + * + * @see http://www.php.net/manual/en/numberformatter.settextattribute.php + * + * @throws MethodNotImplementedException + */ + public function setTextAttribute($attr, $value) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Set the error to the default U_ZERO_ERROR + */ + protected function resetError() + { + StubIntlGlobals::setError(StubIntlGlobals::U_ZERO_ERROR); + $this->errorCode = StubIntlGlobals::getErrorCode(); + $this->errorMessage = StubIntlGlobals::getErrorMessage(); + } + + /** + * Rounds a currency value, applying increment rounding if applicable + * + * When a currency have a rounding increment, an extra round is made after the first one. The rounding factor is + * determined in the ICU data and is explained as of: + * + * "the rounding increment is given in units of 10^(-fraction_digits)" + * + * The only actual rounding data as of this writing, is CHF. + * + * @param float $value The numeric currency value + * @param string $currency The 3-letter ISO 4217 currency code indicating the currency to use + * + * @return string The rounded numeric currency value + * + * @see http://en.wikipedia.org/wiki/Swedish_rounding + * @see http://www.docjar.com/html/api/com/ibm/icu/util/Currency.java.html#1007 + */ + private function roundCurrency($value, $currency) + { + $fractionDigits = Intl::getCurrencyBundle()->getFractionDigits($currency); + $roundingIncrement = Intl::getCurrencyBundle()->getRoundingIncrement($currency); + + // Round with the formatter rounding mode + $value = $this->round($value, $fractionDigits); + + // Swiss rounding + if (0 < $roundingIncrement && 0 < $fractionDigits) { + $roundingFactor = $roundingIncrement / pow(10, $fractionDigits); + $value = round($value / $roundingFactor) * $roundingFactor; + } + + return $value; + } + + /** + * Rounds a value. + * + * @param integer|float $value The value to round + * @param int $precision The number of decimal digits to round to + * + * @return integer|float The rounded value + */ + private function round($value, $precision) + { + $precision = $this->getUnitializedPrecision($value, $precision); + + $roundingMode = self::$phpRoundingMap[$this->getAttribute(self::ROUNDING_MODE)]; + $value = round($value, $precision, $roundingMode); + + return $value; + } + + /** + * Formats a number. + * + * @param integer|float $value The numeric value to format + * @param int $precision The number of decimal digits to use + * + * @return string The formatted number + */ + private function formatNumber($value, $precision) + { + $precision = $this->getUnitializedPrecision($value, $precision); + + return number_format($value, $precision, '.', $this->getAttribute(self::GROUPING_USED) ? ',' : ''); + } + + /** + * Returns the precision value if the the DECIMAL style is being used and the FRACTION_DIGITS attribute is unitialized. + * + * @param integer|float $value The value to get the precision from if the FRACTION_DIGITS attribute is unitialized + * @param int $precision The precision value to returns if the FRACTION_DIGITS attribute is initialized + * + * @return int The precision value + */ + private function getUnitializedPrecision($value, $precision) + { + if ($this->style == self::CURRENCY) { + return $precision; + } + + if (!$this->isInitializedAttribute(self::FRACTION_DIGITS)) { + preg_match('/.*\.(.*)/', (string) $value, $digits); + if (isset($digits[1])) { + $precision = strlen($digits[1]); + } + } + + return $precision; + } + + /** + * Check if the attribute is initialized (value set by client code). + * + * @param string $attr The attribute name + * + * @return Boolean true if the value was set by client, false otherwise + */ + private function isInitializedAttribute($attr) + { + return isset($this->initializedAttributes[$attr]); + } + + /** + * Returns the numeric value using the $type to convert to the right data type. + * + * @param mixed $value The value to be converted + * @param int $type The type to convert. Can be TYPE_DOUBLE (float) or TYPE_INT32 (int) + * + * @return integer|float The converted value + */ + private function convertValueDataType($value, $type) + { + if ($type == self::TYPE_DOUBLE) { + $value = (float) $value; + } elseif ($type == self::TYPE_INT32) { + $value = $this->getInt32Value($value); + } elseif ($type == self::TYPE_INT64) { + $value = $this->getInt64Value($value); + } + + return $value; + } + + /** + * Convert the value data type to int or returns false if the value is out of the integer value range. + * + * @param mixed $value The value to be converted + * + * @return int The converted value + */ + private function getInt32Value($value) + { + if ($value > self::$int32Range['positive'] || $value < self::$int32Range['negative']) { + return false; + } + + return (int) $value; + } + + /** + * Convert the value data type to int or returns false if the value is out of the integer value range. + * + * @param mixed $value The value to be converted + * + * @return int|float The converted value + * + * @see https://bugs.php.net/bug.php?id=59597 Bug #59597 + */ + private function getInt64Value($value) + { + if ($value > self::$int64Range['positive'] || $value < self::$int64Range['negative']) { + return false; + } + + if (PHP_INT_SIZE !== 8 && ($value > self::$int32Range['positive'] || $value <= self::$int32Range['negative'])) { + // Bug #59597 was fixed on PHP 5.3.14 and 5.4.4 + // The negative PHP_INT_MAX was being converted to float + if ( + $value == self::$int32Range['negative'] && + ( + (version_compare(PHP_VERSION, '5.4.0', '<') && version_compare(PHP_VERSION, '5.3.14', '>=')) || + version_compare(PHP_VERSION, '5.4.4', '>=') + ) + ) { + return (int) $value; + } + + return (float) $value; + } + + if (PHP_INT_SIZE === 8) { + // Bug #59597 was fixed on PHP 5.3.14 and 5.4.4 + // A 32 bit integer was being generated instead of a 64 bit integer + if ( + ($value > self::$int32Range['positive'] || $value < self::$int32Range['negative']) && + ( + (version_compare(PHP_VERSION, '5.3.14', '<')) || + (version_compare(PHP_VERSION, '5.4.0', '>=') && version_compare(PHP_VERSION, '5.4.4', '<')) + ) + ) { + $value = (-2147483648 - ($value % -2147483648)) * ($value / abs($value)); + } + } + + return (int) $value; + } + + /** + * Check if the rounding mode is invalid. + * + * @param int $value The rounding mode value to check + * + * @return Boolean true if the rounding mode is invalid, false otherwise + */ + private function isInvalidRoundingMode($value) + { + if (in_array($value, self::$roundingModes, true)) { + return false; + } + + return true; + } + + /** + * Returns the normalized value for the GROUPING_USED attribute. Any value that can be converted to int will be + * cast to Boolean and then to int again. This way, negative values are converted to 1 and string values to 0. + * + * @param mixed $value The value to be normalized + * + * @return int The normalized value for the attribute (0 or 1) + */ + private function normalizeGroupingUsedValue($value) + { + return (int) (Boolean) (int) $value; + } + + /** + * Returns the normalized value for the FRACTION_DIGITS attribute. The value is converted to int and if negative, + * the returned value will be 0. + * + * @param mixed $value The value to be normalized + * + * @return int The normalized value for the attribute + */ + private function normalizeFractionDigitsValue($value) + { + $value = (int) $value; + + return (0 > $value) ? 0 : $value; + } +} diff --git a/src/Symfony/Component/Intl/README.md b/src/Symfony/Component/Intl/README.md new file mode 100644 index 0000000000..33bef34531 --- /dev/null +++ b/src/Symfony/Component/Intl/README.md @@ -0,0 +1,195 @@ +Intl Component +============= + +A PHP replacement layer for the C intl extension that includes additional data +from the ICU library. + +The replacement layer is limited to the locale "en". If you want to use other +locales, you should [install the intl extension] [10] instead. + +Installation +------------ + +You can install the component in two different ways: + +* Using the official Git repository (https://github.com/symfony/Intl); +* [Install it via Composer] [0] (`symfony/intl` on [Packagist] [1]). + +If you install the component via Composer, the following classes and functions +of the intl extension will be automatically provided if the intl extension is +not loaded: + +* [`\Locale`] [2] +* [`\NumberFormatter`] [3] +* [`\IntlDateFormatter`] [4] +* [`\Collator`] [5] +* [`intl_is_failure()`] [6] +* [`intl_get_error_code()`] [7] +* [`intl_get_error_message()`] [8] +* [`intl_error_name()`] [9] + +If you don't use Composer but the Symfony ClassLoader component, you need to +load them manually by adding the following lines to your autoload code: + + if (!function_exists('intl_is_failure')) { + require '/path/to/Icu/Resources/stubs/functions.php'; + + $loader->registerPrefixFallback('/path/to/Icu/Resources/stubs'); + } + +Stubbed Classes +--------------- + +The stubbed classes of the intl extension are limited to the locale "en" and +will throw an exception if you try to use a different locale. For using other +locales, [install the intl extension] [10] instead. + +### Locale + +The only method supported in the [´\Locale`] [2] class is `getDefault()` and +will always return "en". All other methods will throw an exception when used. + +### NumberFormatter + +Numbers can be formatted with the [`\NumberFormatter`] [3] class. The following +methods are supported. All other methods are not supported and will throw an +exception when used. + +##### __construct($locale = 'en', $style = null, $pattern = null) + +The only supported locale is "en". The supported styles are +`\NumberFormatter::DECIMAL` and `\NumberFormatter::CURRENCY`. The argument +`$pattern` may not be used. + +##### ::create($locale = 'en', $style = null, $pattern = null) + +See `__construct()`. + +##### formatCurrency($value, $currency) + +Fully supported. + +##### format($value, $type = \NumberFormatter::TYPE_DEFAULT) + +Only type `\NumberFormatter::TYPE_DEFAULT` is supported. + +##### getAttribute($attr) + +Fully supported. + +##### getErrorCode() + +Fully supported. + +##### getErrorMessage() + +Fully supported. + +##### getLocale($type = \Locale::ACTUAL_LOCALE) + +The parameter `$type` is ignored. + +##### parse($value, $type = \NumberFormatter::TYPE_DOUBLE, &$position = null) + +The supported types are `\NumberFormatter::TYPE_DOUBLE`, +`\NumberFormatter::TYPE_INT32` and `\NumberFormatter::TYPE_INT64`. The +parameter `$position` must always be `null`. + +##### setAttribute($attr, $value) + +The only supported attributes are `\NumberFormatter::FRACTION_DIGITS`, +`\NumberFormatter::GROUPING_USED` and `\NumberFormatter::ROUNDING_MODE`. + +The only supported rounding modes are `\NumberFormatter::ROUND_HALFEVEN`, +`\NumberFormatter::ROUND_HALFDOWN` and `\NumberFormatter::ROUND_HALFUP`. + +Included Resource Bundles +------------------------- + +The ICU data is located in several "resource bundles". You can access a PHP +wrapper of these bundles through the static Intl class. + +Languages and Scripts +~~~~~~~~~~~~~~~~~~~~~ + +The translations of language and script names can be found in the language +bundle. + + $languages = Intl::getLanguageBundle()->getLanguageNames('en'); + // => array('ab' => 'Abkhazian', ...) + + $language = Intl::getLanguageBundle()->getLanguageName('en', 'de'); + // => 'German' + + $language = Intl::getLanguageBundle()->getLanguageName('en', 'de', 'AT); + // => 'Austrian German' + + $scripts = Intl::getLanguageBundle()->getScriptNames('en'); + // => array('Arab' => 'Arabic', ...) + + $script = Intl::getLanguageBundle()->getScriptName('en', 'Hans'); + // => 'Simplified' + +Countries +~~~~~~~~~ + +The translations of country names can be found in the region bundle. + + $countries = Intl::getRegionBundle()->getCountryNames('en'); + // => array('AF' => 'Afghanistan', ...) + + $country = Intl::getRegionBundle()->getCountryName('en', 'GB'); + // => 'United Kingdom' + +Locales +~~~~~~~ + +The translations of locale names can be found in the locale bundle. + + $locales = Intl::getLocaleBundle()->getLocaleNames('en'); + // => array('af' => 'Afrikaans', ...) + + $locale = Intl::getLocaleBundle()->getLocaleName('en', 'zh_Hans_MO'); + // => 'Chinese (Simplified, Macau SAR China)' + +Currencies +~~~~~~~~~~ + +The translations of currency names and other currency-related information can +be found in the currency bundle. + + $currencies = Intl::getCurrencyBundle()->getCurrencyNames('en'); + // => array('AFN' => 'Afghan Afghani', ...) + + $currency = Intl::getCurrencyBundle()->getCurrencyNames('en', 'INR'); + // => 'Indian Rupee' + + $symbol = Intl::getCurrencyBundle()->getCurrencyNames('en', 'INR'); + // => '₹' + + $fractionDigits = Intl::getCurrencyBundle()->getFractionDigits('INR'); + // => 2 + + $roundingIncrement = Intl::getCurrencyBundle()->getRoundingIncrement('INR'); + // => 0 + +Resources +--------- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/Intl/ + $ composer.phar install --dev + $ phpunit + +[0]: /components/using_components +[1]: https://packagist.org/packages/symfony/intl +[2]: http://www.php.net/manual/en/class.locale.php +[3]: http://www.php.net/manual/en/class.numberformatter.php +[4]: http://www.php.net/manual/en/class.intldateformatter.php +[5]: http://www.php.net/manual/en/class.collator.php +[6]: http://www.php.net/manual/en/function.intl-error-name.php +[7]: http://www.php.net/manual/en/function.intl-get-error-code.php +[8]: http://www.php.net/manual/en/function.intl-get-error-message.php +[9]: http://www.php.net/manual/en/function.intl-is-failure.php +[10]: http://www.php.net/manual/en/intl.setup.php diff --git a/src/Symfony/Component/Intl/ResourceBundle/AbstractBundle.php b/src/Symfony/Component/Intl/ResourceBundle/AbstractBundle.php new file mode 100644 index 0000000000..d1d523c40a --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/AbstractBundle.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle; + +use Symfony\Component\Intl\Intl; +use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface; + +/** + * Base class for {@link ResourceBundleInterface} implementations. + * + * @author Bernhard Schussek + */ +abstract class AbstractBundle implements ResourceBundleInterface +{ + /** + * @var string + */ + private $path; + + /** + * @var StructuredBundleReaderInterface + */ + private $reader; + + /** + * Creates a bundle at the given path using the given reader for reading + * bundle entries. + * + * @param string $path The path to the bundle. + * @param StructuredBundleReaderInterface $reader The reader for reading + * the bundle. + */ + public function __construct($path, StructuredBundleReaderInterface $reader) + { + $this->path = $path; + $this->reader = $reader; + } + + /** + * {@inheritdoc} + */ + public function getLocales() + { + return $this->reader->getLocales($this->path); + } + + /** + * Proxy method for {@link StructuredBundleReaderInterface#read}. + */ + protected function read($locale) + { + return $this->reader->read($this->path, $locale); + } + + /** + * Proxy method for {@link StructuredBundleReaderInterface#readEntry}. + */ + protected function readEntry($locale, array $indices, $mergeFallback = false) + { + return $this->reader->readEntry($this->path, $locale, $indices, $mergeFallback); + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Compiler/BundleCompiler.php b/src/Symfony/Component/Intl/ResourceBundle/Compiler/BundleCompiler.php new file mode 100644 index 0000000000..eaa32f4b29 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Compiler/BundleCompiler.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Compiler; + +use Symfony\Component\Intl\Exception\RuntimeException; + +/** + * Compiles .txt resource bundles to binary .res files. + * + * @author Bernhard Schussek + */ +class BundleCompiler implements ResourceBundleCompilerInterface +{ + /** + * @var string The path to the "genrb" executable. + */ + private $genrb; + + /** + * Creates a new compiler based on the "genrb" executable. + * + * @param string $genrb Optional. The path to the "genrb" executable. + * + * @throws RuntimeException If the "genrb" cannot be found. + */ + public function __construct($genrb = 'genrb') + { + exec('which ' . $genrb, $output, $status); + + if (0 !== $status) { + throw new RuntimeException(sprintf( + 'The command "%s" is not installed', + $genrb + )); + } + + $this->genrb = $genrb; + } + + /** + * {@inheritdoc} + */ + public function compile($sourcePath, $targetDir) + { + if (is_dir($sourcePath)) { + $sourcePath .= '/*.txt'; + } + + exec($this->genrb.' --quiet -e UTF-8 -d '.$targetDir.' '.$sourcePath, $output, $status); + + if ($status !== 0) { + throw new RuntimeException(sprintf( + 'genrb failed with status %d while compiling %s to %s.', + $status, + $sourcePath, + $targetDir + )); + } + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Compiler/BundleCompilerInterface.php b/src/Symfony/Component/Intl/ResourceBundle/Compiler/BundleCompilerInterface.php new file mode 100644 index 0000000000..94f629c07e --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Compiler/BundleCompilerInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Compiler; + +/** + * Compiles a resource bundle. + * + * @author Bernhard Schussek + */ +interface ResourceBundleCompilerInterface +{ + /** + * Compiles a resource bundle at the given source to the given target + * directory. + * + * @param string $sourcePath + * @param string $targetDir + */ + public function compile($sourcePath, $targetDir); +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/CurrencyBundle.php b/src/Symfony/Component/Intl/ResourceBundle/CurrencyBundle.php new file mode 100644 index 0000000000..c32e2c4980 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/CurrencyBundle.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle; + +/** + * Default implementation of {@link CurrencyBundleInterface}. + * + * @author Bernhard Schussek + */ +class CurrencyBundle extends AbstractBundle implements CurrencyBundleInterface +{ + const INDEX_NAME = 0; + + const INDEX_SYMBOL = 1; + + const INDEX_FRACTION_DIGITS = 2; + + const INDEX_ROUNDING_INCREMENT = 3; + + /** + * {@inheritdoc} + */ + public function getCurrencySymbol($locale, $currency) + { + return $this->readEntry($locale, array('Currencies', $currency, static::INDEX_SYMBOL)); + } + + /** + * {@inheritdoc} + */ + public function getCurrencyName($locale, $currency) + { + return $this->readEntry($locale, array('Currencies', $currency, static::INDEX_NAME)); + } + + /** + * {@inheritdoc} + */ + public function getCurrencyNames($locale) + { + if (null === ($currencies = $this->readEntry($locale, array('Currencies')))) { + return array(); + } + + if ($currencies instanceof \Traversable) { + $currencies = iterator_to_array($currencies); + } + + $index = static::INDEX_NAME; + + array_walk($currencies, function (&$value) use ($index) { + $value = $value[$index]; + }); + + return $currencies; + } + + /** + * {@inheritdoc} + */ + public function getFractionDigits($currency) + { + return $this->readEntry('en', array('Currencies', $currency, static::INDEX_FRACTION_DIGITS)); + } + + /** + * {@inheritdoc} + */ + public function getRoundingIncrement($currency) + { + return $this->readEntry('en', array('Currencies', $currency, static::INDEX_ROUNDING_INCREMENT)); + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/CurrencyBundleInterface.php b/src/Symfony/Component/Intl/ResourceBundle/CurrencyBundleInterface.php new file mode 100644 index 0000000000..596f6e207d --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/CurrencyBundleInterface.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle; + +/** + * Gives access to currency-related ICU data. + * + * @author Bernhard Schussek + */ +interface CurrencyBundleInterface extends ResourceBundleInterface +{ + /** + * Returns the symbol used for a currency. + * + * @param string $locale The locale to return the result in. + * @param string $currency A currency code (e.g. "EUR"). + * + * @return string|null The currency symbol or NULL if not found. + */ + public function getCurrencySymbol($locale, $currency); + + /** + * Returns the name of a currency. + * + * @param string $locale The locale to return the name in. + * @param string $currency A currency code (e.g. "EUR"). + * + * @return string|null The name of the currency or NULL if not found. + */ + public function getCurrencyName($locale, $currency); + + /** + * Returns the names of all known currencies. + * + * @param string $locale The locale to return the names in. + * + * @return string[] A list of currency names indexed by currency codes. + */ + public function getCurrencyNames($locale); + + /** + * Returns the number of digits after the comma of a currency. + * + * @param string $currency A currency code (e.g. "EUR"). + * + * @return integer|null The number of digits after the comma or NULL if not found. + */ + public function getFractionDigits($currency); + + /** + * Returns the rounding increment of a currency. + * + * The rounding increment indicates to which number a currency is rounded. + * For example, 1230 rounded to the nearest 50 is 1250. 1.234 rounded to the + * nearest 0.65 is 1.3. + * + * @param string $currency A currency code (e.g. "EUR"). + * + * @return float|integer|null The rounding increment or NULL if not found. + */ + public function getRoundingIncrement($currency); +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/LanguageBundle.php b/src/Symfony/Component/Intl/ResourceBundle/LanguageBundle.php new file mode 100644 index 0000000000..1c3dfced57 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/LanguageBundle.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle; + +/** + * Default implementation of {@link LanguageBundleInterface}. + * + * @author Bernhard Schussek + */ +class LanguageBundle extends AbstractBundle implements LanguageBundleInterface +{ + /** + * {@inheritdoc} + */ + public function getLanguageName($locale, $lang, $region = null) + { + if (null === ($languages = $this->readEntry($locale, array('Languages')))) { + return array(); + } + + // Some languages are translated together with their region, + // i.e. "en_GB" is translated as "British English" + if (null !== $region && isset($languages[$lang.'_'.$region])) { + return $languages[$lang.'_'.$region]; + } + + return $languages[$lang]; + } + + /** + * {@inheritdoc} + */ + public function getLanguageNames($locale) + { + if (null === ($languages = $this->readEntry($locale, array('Languages')))) { + return array(); + } + + if ($languages instanceof \Traversable) { + $languages = iterator_to_array($languages); + } + + return $languages; + } + + /** + * {@inheritdoc} + */ + public function getScriptName($locale, $script, $lang = null) + { + $data = $this->read($locale); + + // Some languages are translated together with their script, + // e.g. "zh_Hans" is translated as "Simplified Chinese" + if (null !== $lang && isset($data['Languages'][$lang.'_'.$script])) { + $langName = $data['Languages'][$lang.'_'.$script]; + + // If the script is appended in braces, extract it, e.g. "zh_Hans" + // is translated as "Chinesisch (vereinfacht)" in locale "de" + if (strpos($langName, '(') !== false) { + list($langName, $scriptName) = preg_split('/[\s()]/', $langName, null, PREG_SPLIT_NO_EMPTY); + + return $scriptName; + } + } + + // "af" (Afrikaans) has no "Scripts" block + if (!isset($data['Scripts'][$script])) { + return null; + } + + return $data['Scripts'][$script]; + } + + /** + * {@inheritdoc} + */ + public function getScriptNames($locale) + { + if (null === ($scripts = $this->readEntry($locale, array('Scripts')))) { + return array(); + } + + if ($scripts instanceof \Traversable) { + $scripts = iterator_to_array($scripts); + } + + return $scripts; + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/LanguageBundleInterface.php b/src/Symfony/Component/Intl/ResourceBundle/LanguageBundleInterface.php new file mode 100644 index 0000000000..62a0eba250 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/LanguageBundleInterface.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle; + +/** + * Gives access to language-related ICU data. + * + * @author Bernhard Schussek + */ +interface LanguageBundleInterface extends ResourceBundleInterface +{ + /** + * Returns the name of a language. + * + * @param string $locale The locale to return the name in. + * @param string $lang A language code (e.g. "en"). + * @param string|null $region Optional. A region code (e.g. "US"). + * + * @return string|null The name of the language or NULL if not found. + */ + public function getLanguageName($locale, $lang, $region = null); + + /** + * Returns the names of all known languages. + * + * @param string $locale The locale to return the names in. + * + * @return string[] A list of language names indexed by language codes. + */ + public function getLanguageNames($locale); + + /** + * Returns the name of a script. + * + * @param string $locale The locale to return the name in. + * @param string $script A script code (e.g. "Hans"). + * @param string $lang Optional. A language code (e.g. "zh"). + * + * @return string|null The name of the script or NULL if not found. + */ + public function getScriptName($locale, $script, $lang = null); + + /** + * Returns the names of all known scripts. + * + * @param string $locale The locale to return the names in. + * + * @return string[] A list of script names indexed by script codes. + */ + public function getScriptNames($locale); +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/LocaleBundle.php b/src/Symfony/Component/Intl/ResourceBundle/LocaleBundle.php new file mode 100644 index 0000000000..3f50d876db --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/LocaleBundle.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle; + +/** + * Default implementation of {@link LocaleBundleInterface}. + * + * @author Bernhard Schussek + */ +class LocaleBundle extends AbstractBundle implements LocaleBundleInterface +{ + /** + * {@inheritdoc} + */ + public function getLocaleName($locale, $ofLocale) + { + return $this->readEntry($locale, array('Locales', $ofLocale)); + } + + /** + * {@inheritdoc} + */ + public function getLocaleNames($locale) + { + if (null === ($locales = $this->readEntry($locale, array('Locales')))) { + return array(); + } + + if ($locales instanceof \Traversable) { + $locales = iterator_to_array($locales); + } + + return $locales; + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/LocaleBundleInterface.php b/src/Symfony/Component/Intl/ResourceBundle/LocaleBundleInterface.php new file mode 100644 index 0000000000..d7e536486a --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/LocaleBundleInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle; + +/** + * Gives access to locale-related ICU data. + * + * @author Bernhard Schussek + */ +interface LocaleBundleInterface extends ResourceBundleInterface +{ + /** + * Returns the name of a locale. + * + * @param string $locale The locale to return the name in. + * @param string $ofLocale The locale to return the name of (e.g. "de_AT"). + * + * @return string|null The name of the locale or NULL if not found. + */ + public function getLocaleName($locale, $ofLocale); + + /** + * Returns the names of all known locales. + * + * @param string $locale The locale to return the name in. + * + * @return string[] A list of locale names indexed by locale codes. + */ + public function getLocaleNames($locale); +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/AbstractBundleReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/AbstractBundleReader.php new file mode 100644 index 0000000000..c30693ac57 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/AbstractBundleReader.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Reader; + +/** + * Base class for {@link BundleReaderInterface} implementations. + * + * @author Bernhard Schussek + */ +abstract class AbstractBundleReader implements BundleReaderInterface +{ + /** + * {@inheritdoc} + */ + public function getLocales($path) + { + $extension = '.' . $this->getFileExtension(); + $locales = glob($path . '/*' . $extension); + + // Remove file extension and sort + array_walk($locales, function (&$locale) use ($extension) { $locale = basename($locale, $extension); }); + sort($locales); + + return $locales; + } + + /** + * Returns the extension of locale files in this bundle. + * + * @return string The file extension (without leading dot). + */ + abstract protected function getFileExtension(); +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/BinaryBundleReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/BinaryBundleReader.php new file mode 100644 index 0000000000..56cef806da --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/BinaryBundleReader.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Reader; + +use Symfony\Component\Intl\Exception\RuntimeException; +use Symfony\Component\Intl\ResourceBundle\Util\ArrayAccessibleResourceBundle; + +/** + * Reads binary .res resource bundles. + * + * @author Bernhard Schussek + */ +class BinaryBundleReader extends AbstractBundleReader implements BundleReaderInterface +{ + /** + * {@inheritdoc} + */ + public function read($path, $locale) + { + // Point for future extension: Modify this class so that it works also + // if the \ResourceBundle class is not available. + $bundle = new \ResourceBundle($locale, $path); + + if (null === $bundle) { + throw new RuntimeException(sprintf( + 'Could not load the resource bundle "%s/%s.res".', + $path, + $locale + )); + } + + return new ArrayAccessibleResourceBundle($bundle); + } + + /** + * {@inheritdoc} + */ + protected function getFileExtension() + { + return 'res'; + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/BufferedReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/BufferedReader.php new file mode 100644 index 0000000000..d271830eaa --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/BufferedReader.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Reader; + +use Symfony\Component\Intl\ResourceBundle\Util\RingBuffer; + +/** + * @author Bernhard Schussek + */ +class BufferedReader implements BundleReaderInterface +{ + /** + * @var BundleReaderInterface + */ + private $reader; + + + private $buffer; + + /** + * Buffers a given reader. + * + * @param BundleReaderInterface $reader The reader to buffer. + * @param integer $bufferSize The number of entries to store + * in the buffer. + */ + public function __construct(BundleReaderInterface $reader, $bufferSize) + { + $this->reader = $reader; + $this->buffer = new RingBuffer($bufferSize); + } + + /** + * {@inheritdoc} + */ + public function read($path, $locale) + { + $hash = $path . '//' . $locale; + + if (!isset($this->buffer[$hash])) { + $this->buffer[$hash] = $this->reader->read($path, $locale); + } + + return $this->buffer[$hash]; + } + + /** + * {@inheritdoc} + */ + public function getLocales($path) + { + return $this->reader->getLocales($path); + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/BundleReaderInterface.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/BundleReaderInterface.php new file mode 100644 index 0000000000..bc485cd526 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/BundleReaderInterface.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Reader; + +/** + * Reads resource bundle files. + * + * @author Bernhard Schussek + */ +interface BundleReaderInterface +{ + /** + * Reads a resource bundle. + * + * @param string $path The path to the resource bundle. + * @param string $locale The locale to read. + * + * @return mixed Returns an array or {@link \ArrayAccess} instance for + * complex data, a scalar value otherwise. + */ + public function read($path, $locale); + + /** + * Reads the available locales of a resource bundle. + * + * @param string $path The path to the resource bundle. + * + * @return string[] A list of supported locale codes. + */ + public function getLocales($path); +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/PhpBundleReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/PhpBundleReader.php new file mode 100644 index 0000000000..663bcc9d78 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/PhpBundleReader.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Reader; + +use Symfony\Component\Intl\Exception\InvalidArgumentException; +use Symfony\Component\Intl\Exception\RuntimeException; + +/** + * Reads .php resource bundles. + * + * @author Bernhard Schussek + */ +class PhpBundleReader extends AbstractBundleReader implements BundleReaderInterface +{ + /** + * {@inheritdoc} + */ + public function read($path, $locale) + { + if ('en' !== $locale) { + throw new InvalidArgumentException('Only the locale "en" is supported.'); + } + + $fileName = $path . '/' . $locale . '.php'; + + if (!file_exists($fileName)) { + throw new RuntimeException(sprintf( + 'The resource bundle "%s/%s.php" does not exist.', + $path, + $locale + )); + } + + if (!is_file($fileName)) { + throw new RuntimeException(sprintf( + 'The resource bundle "%s/%s.php" is not a file.', + $path, + $locale + )); + } + + return include $fileName; + } + + /** + * {@inheritdoc} + */ + protected function getFileExtension() + { + return 'php'; + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReader.php new file mode 100644 index 0000000000..ff5dfc839e --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReader.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Reader; + +use Symfony\Component\Intl\ResourceBundle\Util\RecursiveArrayAccess; + +/** + * A structured reader wrapping an existing resource bundle reader. + * + * @author Bernhard Schussek + * + * @see StructuredResourceBundleBundleReaderInterface + */ +class StructuredBundleReader implements StructuredBundleReaderInterface +{ + /** + * @var BundleReaderInterface + */ + private $reader; + + /** + * Creates an entry reader based on the given resource bundle reader. + * + * @param BundleReaderInterface $reader A resource bundle reader to use. + */ + public function __construct(BundleReaderInterface $reader) + { + $this->reader = $reader; + } + + /** + * {@inheritdoc} + */ + public function read($path, $locale) + { + return $this->reader->read($path, $locale); + } + + /** + * {@inheritdoc} + */ + public function getLocales($path) + { + return $this->reader->getLocales($path); + } + + /** + * {@inheritdoc} + */ + public function readEntry($path, $locale, array $indices, $mergeFallback = true) + { + $data = $this->reader->read($path, $locale); + + $entry = RecursiveArrayAccess::get($data, $indices); + $multivalued = is_array($entry) || $entry instanceof \Traversable; + + if (!($mergeFallback && (null === $entry || $multivalued))) { + return $entry; + } + + if (null !== ($fallbackLocale = $this->getFallbackLocale($locale))) { + $parentEntry = $this->readEntry($path, $fallbackLocale, $indices, true); + + if ($entry || $parentEntry) { + $multivalued = $multivalued || is_array($parentEntry) || $parentEntry instanceof \Traversable; + + if ($multivalued) { + if ($entry instanceof \Traversable) { + $entry = iterator_to_array($entry); + } + + if ($parentEntry instanceof \Traversable) { + $parentEntry = iterator_to_array($parentEntry); + } + + $entry = array_merge( + $parentEntry ?: array(), + $entry ?: array() + ); + } else { + $entry = null === $entry ? $parentEntry : $entry; + } + } + } + + return $entry; + } + + /** + * Returns the fallback locale for a given locale, if any + * + * @param string $locale The locale to find the fallback for. + * + * @return string|null The fallback locale, or null if no parent exists + */ + private function getFallbackLocale($locale) + { + if (false === $pos = strrpos($locale, '_')) { + return null; + } + + return substr($locale, 0, $pos); + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReaderInterface.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReaderInterface.php new file mode 100644 index 0000000000..900690f813 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReaderInterface.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Reader; + +/** + * Reads individual entries of a resource file. + * + * @author Bernhard Schussek + */ +interface StructuredBundleReaderInterface extends BundleReaderInterface +{ + /** + * Reads an entry from a resource bundle. + * + * An entry can be selected from the resource bundle by passing the path + * to that entry in the bundle. For example, if the bundle is structured + * like this: + * + * TopLevel + * NestedLevel + * Entry: Value + * + * Then the value can be read by calling: + * + * $reader->readEntry('...', 'en', array('TopLevel', 'NestedLevel', 'Entry')); + * + * @param string $path The path to the resource bundle. + * @param string $locale The locale to read. + * @param string[] $indices The indices to read from the bundle. + * @param Boolean $mergeFallback Whether to merge the value with the value + * from the fallback locale (e.g. "en" for + * "en_GB"). Only applicable if the result + * is multivalued (array, \ArrayAccess). + * + * @return mixed Returns an array or {@link \ArrayAccess} instance for + * complex data, a scalar value for simple data and NULL + * if the given path could not be accessed. + */ + public function readEntry($path, $locale, array $indices, $mergeFallback = true); +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/RegionBundle.php b/src/Symfony/Component/Intl/ResourceBundle/RegionBundle.php new file mode 100644 index 0000000000..58ccfd4dc7 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/RegionBundle.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle; + +/** + * Default implementation of {@link RegionBundleInterface}. + * + * @author Bernhard Schussek + */ +class RegionBundle extends AbstractBundle implements RegionBundleInterface +{ + /** + * {@inheritdoc} + */ + public function getCountryName($locale, $country) + { + return $this->readEntry($locale, array('Countries', $country)); + } + + /** + * {@inheritdoc} + */ + public function getCountryNames($locale) + { + if (null === ($countries = $this->readEntry($locale, array('Countries')))) { + return array(); + } + + if ($countries instanceof \Traversable) { + $countries = iterator_to_array($countries); + } + + return $countries; + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/RegionBundleInterface.php b/src/Symfony/Component/Intl/ResourceBundle/RegionBundleInterface.php new file mode 100644 index 0000000000..835748b568 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/RegionBundleInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle; + +/** + * Gives access to region-related ICU data. + * + * @author Bernhard Schussek + */ +interface RegionBundleInterface extends ResourceBundleInterface +{ + /** + * Returns the name of a country. + * + * @param string $locale The locale to return the name in. + * @param string $country A country code (e.g. "US"). + * + * @return string|null The name of the country or NULL if not found. + */ + public function getCountryName($locale, $country); + + /** + * Returns the names of all known countries. + * + * @param string $locale The locale to return the names in. + * + * @return string[] A list of country names indexed by country codes. + */ + public function getCountryNames($locale); +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/ResourceBundleInterface.php b/src/Symfony/Component/Intl/ResourceBundle/ResourceBundleInterface.php new file mode 100644 index 0000000000..497a66a312 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/ResourceBundleInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle; + +/** + * Gives access to ICU data. + * + * @author Bernhard Schussek + */ +interface ResourceBundleInterface +{ + /** + * Returns the list of locales that this bundle supports. + * + * @return string[] A list of locale codes. + */ + public function getLocales(); +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Stub/StubCurrencyBundle.php b/src/Symfony/Component/Intl/ResourceBundle/Stub/StubCurrencyBundle.php new file mode 100644 index 0000000000..52c8454d8b --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Stub/StubCurrencyBundle.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Stub; + +use Symfony\Component\Intl\ResourceBundle\CurrencyBundle; +use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface; + +/** + * @author Bernhard Schussek + */ +class StubCurrencyBundle extends CurrencyBundle +{ + public function __construct(StructuredBundleReaderInterface $reader) + { + parent::__construct(realpath(__DIR__ . '/../../Resources/data/curr'), $reader); + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Stub/StubLanguageBundle.php b/src/Symfony/Component/Intl/ResourceBundle/Stub/StubLanguageBundle.php new file mode 100644 index 0000000000..faf58c303f --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Stub/StubLanguageBundle.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Stub; + +use Symfony\Component\Intl\ResourceBundle\LanguageBundle; +use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface; + +/** + * @author Bernhard Schussek + */ +class StubLanguageBundle extends LanguageBundle +{ + public function __construct(StructuredBundleReaderInterface $reader) + { + parent::__construct(realpath(__DIR__ . '/../../Resources/data/lang'), $reader); + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Stub/StubLocaleBundle.php b/src/Symfony/Component/Intl/ResourceBundle/Stub/StubLocaleBundle.php new file mode 100644 index 0000000000..f3ba9ce4f5 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Stub/StubLocaleBundle.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Stub; + +use Symfony\Component\Intl\ResourceBundle\LocaleBundle; +use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface; + +/** + * @author Bernhard Schussek + */ +class StubLocaleBundle extends LocaleBundle +{ + public function __construct(StructuredBundleReaderInterface $reader) + { + parent::__construct(realpath(__DIR__ . '/../../Resources/data/locales'), $reader); + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Stub/StubRegionBundle.php b/src/Symfony/Component/Intl/ResourceBundle/Stub/StubRegionBundle.php new file mode 100644 index 0000000000..49f747e7de --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Stub/StubRegionBundle.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Stub; + +use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface; +use Symfony\Component\Intl\ResourceBundle\RegionBundle; + +/** + * @author Bernhard Schussek + */ +class StubRegionBundle extends RegionBundle +{ + public function __construct(StructuredBundleReaderInterface $reader) + { + parent::__construct(realpath(__DIR__ . '/../../Resources/data/region'), $reader); + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/BundleTransformer.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/BundleTransformer.php new file mode 100644 index 0000000000..0692d6fe50 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/BundleTransformer.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Transformer; + +use Symfony\Component\Intl\Exception\RuntimeException; +use Symfony\Component\Intl\ResourceBundle\Transformer\Rule\TransformationRuleInterface; +use Symfony\Component\Intl\ResourceBundle\Writer\PhpBundleWriter; + +/** + * Compiles a number of resource bundles based on predefined compilation rules. + * + * @author Bernhard Schussek + */ +class BundleTransformer +{ + /** + * @var TransformationRuleInterface[] + */ + private $rules = array(); + + /** + * Adds a new compilation rule. + * + * @param TransformationRuleInterface $rule The compilation rule. + */ + public function addRule(TransformationRuleInterface $rule) + { + $this->rules[] = $rule; + } + + /** + * Runs the compilation with the given compilation context. + * + * @param CompilationContextInterface $context The context storing information + * needed to run the compilation. + * + * @throws RuntimeException If any of the files to be compiled by the loaded + * compilation rules does not exist. + */ + public function compileBundles(CompilationContextInterface $context) + { + $filesystem = $context->getFilesystem(); + $compiler = $context->getCompiler(); + + $filesystem->remove($context->getBinaryDir()); + $filesystem->mkdir($context->getBinaryDir()); + + foreach ($this->rules as $rule) { + $filesystem->mkdir($context->getBinaryDir() . '/' . $rule->getBundleName()); + + $resources = (array) $rule->beforeCompile($context); + + foreach ($resources as $resource) { + if (!file_exists($resource)) { + throw new RuntimeException(sprintf( + 'The file "%s" to be compiled by %s does not exist.', + $resource, + get_class($rule) + )); + } + + $compiler->compile($resource, $context->getBinaryDir() . '/' . $rule->getBundleName()); + } + + $rule->afterCompile($context); + } + } + + public function createStubs(StubbingContextInterface $context) + { + $filesystem = $context->getFilesystem(); + $phpWriter = new PhpBundleWriter(); + + $filesystem->remove($context->getStubDir()); + $filesystem->mkdir($context->getStubDir()); + + foreach ($this->rules as $rule) { + $filesystem->mkdir($context->getStubDir() . '/' . $rule->getBundleName()); + + $data = $rule->beforeCreateStub($context); + + $phpWriter->write($context->getStubDir() . '/' . $rule->getBundleName(), 'en', $data); + + $rule->afterCreateStub($context); + } + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/CompilationContext.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/CompilationContext.php new file mode 100644 index 0000000000..f0bf136b6e --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/CompilationContext.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Transformer; + +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Intl\ResourceBundle\Compiler\ResourceBundleCompilerInterface; + +/** + * Default implementation of {@link CompilationContextInterface}. + * + * @author Bernhard Schussek + */ +class CompilationContext implements CompilationContextInterface +{ + /** + * @var string + */ + private $sourceDir; + + /** + * @var string + */ + private $binaryDir; + + /** + * @var FileSystem + */ + private $filesystem; + + /** + * @var ResourceBundleCompilerInterface + */ + private $compiler; + + /** + * @var string + */ + private $icuVersion; + + public function __construct($sourceDir, $binaryDir, Filesystem $filesystem, ResourceBundleCompilerInterface $compiler, $icuVersion) + { + $this->sourceDir = $sourceDir; + $this->binaryDir = $binaryDir; + $this->filesystem = $filesystem; + $this->compiler = $compiler; + $this->icuVersion = $icuVersion; + } + + /** + * {@inheritdoc} + */ + public function getSourceDir() + { + return $this->sourceDir; + } + + /** + * {@inheritdoc} + */ + public function getBinaryDir() + { + return $this->binaryDir; + } + + /** + * {@inheritdoc} + */ + public function getFilesystem() + { + return $this->filesystem; + } + + /** + * {@inheritdoc} + */ + public function getCompiler() + { + return $this->compiler; + } + + /** + * {@inheritdoc} + */ + public function getIcuVersion() + { + return $this->icuVersion; + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/CompilationContextInterface.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/CompilationContextInterface.php new file mode 100644 index 0000000000..cbda8ebc18 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/CompilationContextInterface.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Transformer; + +/** + * Stores contextual information for resource bundle compilation. + * + * @author Bernhard Schussek + */ +interface CompilationContextInterface +{ + /** + * Returns the directory where the source versions of the resource bundles + * are stored. + * + * @return string An absolute path to a directory. + */ + public function getSourceDir(); + + /** + * Returns the directory where the binary resource bundles are stored. + * + * @return string An absolute path to a directory. + */ + public function getBinaryDir(); + + /** + * Returns a tool for manipulating the filesystem. + * + * @return \Symfony\Component\Filesystem\Filesystem The filesystem manipulator. + */ + public function getFilesystem(); + + /** + * Returns a resource bundle compiler. + * + * @return \Symfony\Component\Intl\ResourceBundle\Compiler\ResourceBundleCompilerInterface The loaded resource bundle compiler. + */ + public function getCompiler(); + + /** + * Returns the ICU version of the bundles being converted. + * + * @return string The ICU version string. + */ + public function getIcuVersion(); +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/CurrencyBundleTransformationRule.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/CurrencyBundleTransformationRule.php new file mode 100644 index 0000000000..755f1fd261 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/CurrencyBundleTransformationRule.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Transformer\Rule; + +use Symfony\Component\Intl\Intl; +use Symfony\Component\Intl\ResourceBundle\CurrencyBundle; +use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface; +use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface; + +/** + * The rule for compiling the currency bundle. + * + * @author Bernhard Schussek + */ +class CurrencyBundleTransformationRule implements TransformationRuleInterface +{ + /** + * {@inheritdoc} + */ + public function getBundleName() + { + return 'curr'; + } + + /** + * {@inheritdoc} + */ + public function beforeCompile(CompilationContextInterface $context) + { + // The currency data is contained in the locales and misc bundles + // in ICU <= 4.2 + if (version_compare($context->getIcuVersion(), '4.2', '<=')) { + return array( + $context->getSourceDir() . '/misc/supplementalData.txt', + $context->getSourceDir() . '/locales' + ); + } + + return $context->getSourceDir() . '/curr'; + } + + /** + * {@inheritdoc} + */ + public function afterCompile(CompilationContextInterface $context) + { + // \ResourceBundle does not like locale names with uppercase chars, so rename + // the resource file + // See: http://bugs.php.net/bug.php?id=54025 + $fileName = $context->getBinaryDir() . '/curr/supplementalData.res'; + $fileNameLower = $context->getBinaryDir() . '/curr/supplementaldata.res'; + + $context->getFilesystem()->rename($fileName, $fileNameLower); + } + + /** + * {@inheritdoc} + */ + public function beforeCreateStub(StubbingContextInterface $context) + { + $currencies = array(); + $currencyBundle = Intl::getCurrencyBundle(); + + foreach ($currencyBundle->getCurrencyNames('en') as $code => $name) { + $currencies[$code] = array( + CurrencyBundle::INDEX_NAME => $name, + CurrencyBundle::INDEX_SYMBOL => $currencyBundle->getCurrencySymbol('en', $code), + CurrencyBundle::INDEX_FRACTION_DIGITS => $currencyBundle->getFractionDigits($code), + CurrencyBundle::INDEX_ROUNDING_INCREMENT => $currencyBundle->getRoundingIncrement($code), + ); + } + + return array( + 'Currencies' => $currencies, + ); + } + + /** + * {@inheritdoc} + */ + public function afterCreateStub(StubbingContextInterface $context) + { + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/LanguageBundleTransformationRule.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/LanguageBundleTransformationRule.php new file mode 100644 index 0000000000..44ef53b883 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/LanguageBundleTransformationRule.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Transformer\Rule; + +use Symfony\Component\Intl\Intl; +use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface; +use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface; + +/** + * The rule for compiling the language bundle. + * + * @author Bernhard Schussek + */ +class LanguageBundleTransformationRule implements TransformationRuleInterface +{ + /** + * {@inheritdoc} + */ + public function getBundleName() + { + return 'lang'; + } + + /** + * {@inheritdoc} + */ + public function beforeCompile(CompilationContextInterface $context) + { + // The language data is contained in the locales bundle in ICU <= 4.2 + if (version_compare($context->getIcuVersion(), '4.2', '<=')) { + return $context->getSourceDir() . '/locales'; + } + + return $context->getSourceDir() . '/lang'; + } + + /** + * {@inheritdoc} + */ + public function afterCompile(CompilationContextInterface $context) + { + } + + /** + * {@inheritdoc} + */ + public function beforeCreateStub(StubbingContextInterface $context) + { + return array( + 'Languages' => Intl::getLanguageBundle()->getLanguageNames('en'), + 'Scripts' => Intl::getLanguageBundle()->getScriptNames('en'), + ); + } + + /** + * {@inheritdoc} + */ + public function afterCreateStub(StubbingContextInterface $context) + { + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/LocaleBundleTransformationRule.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/LocaleBundleTransformationRule.php new file mode 100644 index 0000000000..4d4c4a8dfa --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/LocaleBundleTransformationRule.php @@ -0,0 +1,251 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Transformer\Rule; + +use Symfony\Component\Intl\Exception\RuntimeException; +use Symfony\Component\Intl\Intl; +use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface; +use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface; +use Symfony\Component\Intl\ResourceBundle\Writer\TextBundleWriter; + +/** + * The rule for compiling the locale bundle. + * + * @author Bernhard Schussek + */ +class LocaleBundleTransformationRule implements TransformationRuleInterface +{ + /** + * @var \Symfony\Component\Intl\ResourceBundle\LanguageBundleInterface + */ + private $languageBundle; + + /** + * @var \Symfony\Component\Intl\ResourceBundle\RegionBundleInterface + */ + private $regionBundle; + + public function __construct() + { + $this->languageBundle = Intl::getLanguageBundle(); + $this->regionBundle = Intl::getRegionBundle(); + } + + /** + * {@inheritdoc} + */ + public function getBundleName() + { + return 'locales'; + } + + /** + * {@inheritdoc} + */ + public function beforeCompile(CompilationContextInterface $context) + { + $tempDir = sys_get_temp_dir() . '/icu-data-locales'; + + $context->getFilesystem()->remove($tempDir); + $context->getFilesystem()->mkdir($tempDir); + + $this->generateTextFiles($tempDir, $this->scanLocales($context)); + + return $tempDir; + } + + /** + * {@inheritdoc} + */ + public function afterCompile(CompilationContextInterface $context) + { + $context->getFilesystem()->remove(sys_get_temp_dir() . '/icu-data-locales'); + } + + /** + * {@inheritdoc} + */ + public function beforeCreateStub(StubbingContextInterface $context) + { + return array( + 'Locales' => Intl::getLocaleBundle()->getLocaleNames('en'), + ); + } + + /** + * {@inheritdoc} + */ + public function afterCreateStub(StubbingContextInterface $context) + { + } + + private function scanLocales(CompilationContextInterface $context) + { + $tempDir = sys_get_temp_dir() . '/icu-data-locales-source'; + + $context->getFilesystem()->remove($tempDir); + $context->getFilesystem()->mkdir($tempDir); + + // Temporarily generate the resource bundles + $context->getCompiler()->compile($context->getSourceDir() . '/locales', $tempDir); + + // Discover the list of supported locales, which are the names of the resource + // bundles in the "locales" directory + $locales = glob($tempDir . '/*.res'); + + // Remove file extension and sort + array_walk($locales, function (&$locale) { $locale = basename($locale, '.res'); }); + sort($locales); + + // Delete unneeded locales + foreach ($locales as $key => $locale) { + // Delete all aliases from the list + // i.e., "az_AZ" is an alias for "az_Latn_AZ" + $content = file_get_contents($context->getSourceDir() . '/locales/' . $locale . '.txt'); + + // The key "%%ALIAS" is not accessible through the \ResourceBundle class, + // so look in the original .txt file instead + if (strpos($content, '%%ALIAS') !== false) { + unset($locales[$key]); + } + + // Delete locales that have no content (i.e. only "Version" key) + $bundle = new \ResourceBundle($locale, $tempDir); + + if (null === $bundle) { + throw new RuntimeException('The resource bundle for locale ' . $locale . ' could not be loaded from directory ' . $tempDir); + } + + // There seems to be no other way for identifying all keys in this specific + // resource bundle + if (array_keys(iterator_to_array($bundle)) === array('Version')) { + unset($locales[$key]); + } + } + + $context->getFilesystem()->remove($tempDir); + + return $locales; + } + + private function generateTextFiles($targetDirectory, array $locales) + { + $displayLocales = array_unique(array_merge( + $this->languageBundle->getLocales(), + $this->regionBundle->getLocales() + )); + + $txtWriter = new TextBundleWriter(); + + // Generate a list of locale names in the language of each display locale + // Each locale name has the form: "Language (Script, Region, Variant1, ...) + // Script, Region and Variants are optional. If none of them is available, + // the braces are not printed. + foreach ($displayLocales as $displayLocale) { + // Don't include ICU's root resource bundle + if ('root' === $displayLocale) { + continue; + } + + $names = array(); + + foreach ($locales as $locale) { + // Don't include ICU's root resource bundle + if ($locale === 'root') { + continue; + } + + if (null !== ($name = $this->generateLocaleName($locale, $displayLocale))) { + $names[$locale] = $name; + } + } + + // If no names could be generated for the current locale, skip it + if (0 === count($names)) { + continue; + } + + $txtWriter->write($targetDirectory, $displayLocale, array('Locales' => $names)); + } + } + + private function generateLocaleName($locale, $displayLocale) + { + $name = null; + + $lang = \Locale::getPrimaryLanguage($locale); + $script = \Locale::getScript($locale); + $region = \Locale::getRegion($locale); + $variants = \Locale::getAllVariants($locale); + + // Currently the only available variant is POSIX, which we don't want + // to include in the list + if (count($variants) > 0) { + return null; + } + + // Some languages are translated together with their region, + // i.e. "en_GB" is translated as "British English" + // we don't include these languages though because they mess up + // the name sorting + // $name = $this->langBundle->getLanguageName($displayLocale, $lang, $region); + + // Some languages are simply not translated + // Example: "az" (Azerbaijani) has no translation in "af" (Afrikaans) + if (null === ($name = $this->languageBundle->getLanguageName($displayLocale, $lang))) { + return null; + } + + // "as" (Assamese) has no "Variants" block + //if (!$langBundle->get('Variants')) { + // continue; + //} + + $extras = array(); + + // Discover the name of the script part of the locale + // i.e. in zh_Hans_MO, "Hans" is the script + if ($script) { + // Some scripts are not translated into every language + if (null === ($scriptName = $this->languageBundle->getScriptName($displayLocale, $script, $lang))) { + return null; + } + + $extras[] = $scriptName; + } + + // Discover the name of the region part of the locale + // i.e. in de_AT, "AT" is the region + if ($region) { + // Some regions are not translated into every language + if (null === ($regionName = $this->regionBundle->getCountryName($displayLocale, $region))) { + return null; + } + + $extras[] = $regionName; + } + + if (count($extras) > 0) { + // Remove any existing extras + // For example, in German, zh_Hans is "Chinesisch (vereinfacht)". + // The latter is the script part which is already included in the + // extras and will be appended again with the other extras. + if (preg_match('/^(.+)\s+\([^\)]+\)$/', $name, $matches)) { + $name = $matches[1]; + } + + $name .= ' ('.implode(', ', $extras).')'; + } + + return $name; + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/RegionBundleTransformationRule.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/RegionBundleTransformationRule.php new file mode 100644 index 0000000000..0d986e5543 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/RegionBundleTransformationRule.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Transformer\Rule; + +use Symfony\Component\Intl\Intl; +use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface; +use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface; + +/** + * The rule for compiling the region bundle. + * + * @author Bernhard Schussek + */ +class RegionBundleTransformationRule implements TransformationRuleInterface +{ + /** + * {@inheritdoc} + */ + public function getBundleName() + { + return 'region'; + } + + /** + * {@inheritdoc} + */ + public function beforeCompile(CompilationContextInterface $context) + { + // The region data is contained in the locales bundle in ICU <= 4.2 + if (version_compare($context->getIcuVersion(), '4.2', '<=')) { + return $context->getSourceDir() . '/locales'; + } + + return $context->getSourceDir() . '/region'; + } + + /** + * {@inheritdoc} + */ + public function afterCompile(CompilationContextInterface $context) + { + } + + /** + * {@inheritdoc} + */ + public function beforeCreateStub(StubbingContextInterface $context) + { + return array( + 'Countries' => Intl::getRegionBundle()->getCountryNames('en'), + ); + } + + /** + * {@inheritdoc} + */ + public function afterCreateStub(StubbingContextInterface $context) + { + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/TransformationRuleInterface.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/TransformationRuleInterface.php new file mode 100644 index 0000000000..3965e0d2b7 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/TransformationRuleInterface.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Transformer\Rule; + +use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface; +use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface; + +/** + * Contains instruction for compiling a resource bundle. + * + * @author Bernhard Schussek + */ +interface TransformationRuleInterface +{ + /** + * Returns the name of the compiled resource bundle. + * + * @return string The name of the bundle. + */ + public function getBundleName(); + + /** + * Runs instructions to be executed before compiling the sources of the + * resource bundle. + * + * @param CompilationContextInterface $context The contextual information of + * the compilation. + * + * @return string[] The source directories/files of the bundle. + */ + public function beforeCompile(CompilationContextInterface $context); + + /** + * Runs instructions to be executed after compiling the sources of the + * resource bundle. + * + * @param CompilationContextInterface $context The contextual information of + * the compilation. + */ + public function afterCompile(CompilationContextInterface $context); + + /** + * Runs instructions to be executed before creating the stub version of the + * resource bundle. + * + * @param StubbingContextInterface $context The contextual information of + * the compilation. + * + * @return mixed The data to include in the stub version. + */ + public function beforeCreateStub(StubbingContextInterface $context); + + /** + * Runs instructions to be executed after creating the stub version of the + * resource bundle. + * + * @param StubbingContextInterface $context The contextual information of + * the compilation. + */ + public function afterCreateStub(StubbingContextInterface $context); +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/StubbingContext.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/StubbingContext.php new file mode 100644 index 0000000000..25ab68dbfc --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/StubbingContext.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Transformer; + +use Symfony\Component\Filesystem\Filesystem; + +/** + * @author Bernhard Schussek + */ +class StubbingContext implements StubbingContextInterface +{ + /** + * @var string + */ + private $binaryDir; + + /** + * @var string + */ + private $stubDir; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var string + */ + private $icuVersion; + + public function __construct($binaryDir, $stubDir, Filesystem $filesystem, $icuVersion) + { + $this->binaryDir = $binaryDir; + $this->stubDir = $stubDir; + $this->filesystem = $filesystem; + $this->icuVersion = $icuVersion; + } + + /** + * {@inheritdoc} + */ + public function getBinaryDir() + { + return $this->binaryDir; + } + + /** + * {@inheritdoc} + */ + public function getStubDir() + { + return $this->stubDir; + } + + /** + * {@inheritdoc} + */ + public function getFilesystem() + { + return $this->filesystem; + } + + /** + * {@inheritdoc} + */ + public function getIcuVersion() + { + return $this->icuVersion; + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/StubbingContextInterface.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/StubbingContextInterface.php new file mode 100644 index 0000000000..dc49255620 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/StubbingContextInterface.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Transformer; + +/** + * @author Bernhard Schussek + */ +interface StubbingContextInterface +{ + /** + * Returns the directory where the binary resource bundles are stored. + * + * @return string An absolute path to a directory. + */ + public function getBinaryDir(); + + /** + * Returns the directory where the stub resource bundles are stored. + * + * @return string An absolute path to a directory. + */ + public function getStubDir(); + + /** + * Returns a tool for manipulating the filesystem. + * + * @return \Symfony\Component\Filesystem\Filesystem The filesystem manipulator. + */ + public function getFilesystem(); + + /** + * Returns the ICU version of the bundles being converted. + * + * @return string The ICU version string. + */ + public function getIcuVersion(); +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Util/ArrayAccessibleResourceBundle.php b/src/Symfony/Component/Intl/ResourceBundle/Util/ArrayAccessibleResourceBundle.php new file mode 100644 index 0000000000..9a4cccb461 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Util/ArrayAccessibleResourceBundle.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Util; + +use Symfony\Component\Intl\Exception\BadMethodCallException; + +/** + * Work-around for a bug in PHP's \ResourceBundle implementation. + * + * More information can be found on https://bugs.php.net/bug.php?id=64356. + * This class can be removed once that bug is fixed. + * + * @author Bernhard Schussek + */ +class ArrayAccessibleResourceBundle implements \ArrayAccess, \IteratorAggregate, \Countable +{ + private $bundleImpl; + + public function __construct(\ResourceBundle $bundleImpl) + { + $this->bundleImpl = $bundleImpl; + } + + public function get($offset, $fallback = null) + { + $value = $this->bundleImpl->get($offset, $fallback); + + return $value instanceof \ResourceBundle ? new static($value) : $value; + } + + public function offsetExists($offset) + { + return null !== $this->bundleImpl[$offset]; + } + + public function offsetGet($offset) + { + return $this->get($offset); + } + + public function offsetSet($offset, $value) + { + throw new BadMethodCallException('Resource bundles cannot be modified.'); + } + + public function offsetUnset($offset) + { + throw new BadMethodCallException('Resource bundles cannot be modified.'); + } + + public function getIterator() + { + return $this->bundleImpl; + } + + public function count() + { + return $this->bundleImpl->count(); + } + + public function getErrorCode() + { + return $this->bundleImpl->getErrorCode(); + } + + public function getErrorMessage() + { + return $this->bundleImpl->getErrorMessage(); + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Util/RecursiveArrayAccess.php b/src/Symfony/Component/Intl/ResourceBundle/Util/RecursiveArrayAccess.php new file mode 100644 index 0000000000..e1feaa2ce0 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Util/RecursiveArrayAccess.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Util; + +/** + * @author Bernhard Schussek + */ +class RecursiveArrayAccess +{ + public static function get($array, array $indices) + { + foreach ($indices as $index) { + if (!$array instanceof \ArrayAccess && !is_array($array)) { + return null; + } + + $array = $array[$index]; + } + + return $array; + } + + private function __construct() {} +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Util/RingBuffer.php b/src/Symfony/Component/Intl/ResourceBundle/Util/RingBuffer.php new file mode 100644 index 0000000000..7ccbd1e702 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Util/RingBuffer.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Util; + +use Symfony\Component\Intl\Exception\OutOfBoundsException; + +/** + * Implements a ring buffer. + * + * A ring buffer is an array-like structure with a fixed size. If the buffer + * is full, the next written element overwrites the first bucket in the buffer, + * then the second and so on. + * + * @author Bernhard Schussek + */ +class RingBuffer implements \ArrayAccess +{ + private $values = array(); + + private $indices = array(); + + private $cursor = 0; + + private $size; + + public function __construct($size) + { + $this->size = $size; + } + + /** + * {@inheritdoc} + */ + public function offsetExists($key) + { + return isset($this->indices[$key]); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($key) + { + if (!isset($this->indices[$key])) { + throw new OutOfBoundsException(sprintf( + 'The index "%s" does not exist.', + $key + )); + } + + return $this->values[$this->indices[$key]]; + } + + /** + * {@inheritdoc} + */ + public function offsetSet($key, $value) + { + if (false !== ($keyToRemove = array_search($this->cursor, $this->indices))) { + unset($this->indices[$keyToRemove]); + } + + $this->values[$this->cursor] = $value; + $this->indices[$key] = $this->cursor; + + $this->cursor = ($this->cursor + 1) % $this->size; + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($key) + { + if (isset($this->indices[$key])) { + $this->values[$this->indices[$key]] = null; + unset($this->indices[$key]); + } + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Writer/BundleWriterInterface.php b/src/Symfony/Component/Intl/ResourceBundle/Writer/BundleWriterInterface.php new file mode 100644 index 0000000000..cc3b958657 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Writer/BundleWriterInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Writer; + +/** + * Writes resource bundle files. + * + * @author Bernhard Schussek + */ +interface BundleWriterInterface +{ + /** + * Writes data to a resource bundle. + * + * @param string $path The path to the resource bundle. + * @param string $locale The locale to (over-)write. + * @param mixed $data The data to write. + */ + public function write($path, $locale, $data); +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Writer/PhpBundleWriter.php b/src/Symfony/Component/Intl/ResourceBundle/Writer/PhpBundleWriter.php new file mode 100644 index 0000000000..5738f1daff --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Writer/PhpBundleWriter.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Writer; + +/** + * Writes .php resource bundles. + * + * @author Bernhard Schussek + */ +class PhpBundleWriter implements BundleWriterInterface +{ + /** + * {@inheritdoc} + */ + function write($path, $locale, $data) + { + $template = <<