From 1255a42cfed9ce419962c6cf29181a66c7e22bb8 Mon Sep 17 00:00:00 2001 From: ArthurHoaro <arthur@hoa.ro> Date: Sat, 7 Jan 2017 14:28:58 +0100 Subject: Improve autoLocale() detection - Creates arrays_combination function to cover all cases - add the underscore separator in the regex - add `utf8` encoding in addition to `UTF-8` --- application/Utils.php | 51 ++++++++++++++++++++++++++++++++++++++++++--------- tests/UtilsTest.php | 20 ++++++++++++++++++++ 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/application/Utils.php b/application/Utils.php index 35d65224..19fb7116 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -216,22 +216,55 @@ function is_session_id_valid($sessionId) function autoLocale($headerLocale) { // Default if browser does not send HTTP_ACCEPT_LANGUAGE - $attempts = array('en_US'); + $attempts = array('en_US', 'en_US.utf8', 'en_US.UTF-8'); if (isset($headerLocale)) { // (It's a bit crude, but it works very well. Preferred language is always presented first.) - if (preg_match('/([a-z]{2})-?([a-z]{2})?/i', $headerLocale, $matches)) { - $loc = $matches[1] . (!empty($matches[2]) ? '_' . strtoupper($matches[2]) : ''); - $attempts = array( - $loc.'.UTF-8', $loc, str_replace('_', '-', $loc).'.UTF-8', str_replace('_', '-', $loc), - $loc . '_' . strtoupper($loc).'.UTF-8', $loc . '_' . strtoupper($loc), - $loc . '_' . $loc.'.UTF-8', $loc . '_' . $loc, $loc . '-' . strtoupper($loc).'.UTF-8', - $loc . '-' . strtoupper($loc), $loc . '-' . $loc.'.UTF-8', $loc . '-' . $loc - ); + if (preg_match('/([a-z]{2,3})[-_]?([a-z]{2})?/i', $headerLocale, $matches)) { + $first = [strtolower($matches[1]), strtoupper($matches[1])]; + $separators = ['_', '-']; + $encodings = ['utf8', 'UTF-8']; + if (!empty($matches[2])) { + $second = [strtoupper($matches[2]), strtolower($matches[2])]; + $attempts = arrays_combination([$first, $separators, $second, ['.'], $encodings]); + } else { + $attempts = arrays_combination([$first, $separators, $first, ['.'], $encodings]); + } } } setlocale(LC_ALL, $attempts); } +/** + * Combine multiple arrays of string to get all possible strings. + * The order is important because this doesn't shuffle the entries. + * + * Example: + * [['a'], ['b', 'c']] + * will generate: + * - ab + * - ac + * + * TODO PHP 5.6: use the `...` operator instead of an array of array. + * + * @param array $items array of array of string + * + * @return array Combined string from the input array. + */ +function arrays_combination($items) +{ + $out = ['']; + foreach ($items as $item) { + $add = []; + foreach ($item as $element) { + foreach ($out as $key => $existingEntry) { + $add[] = $existingEntry . $element; + } + } + $out = $add; + } + return $out; +} + /** * Generates a default API secret. * diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index c885f552..b8f608b9 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php @@ -282,4 +282,24 @@ class UtilsTest extends PHPUnit_Framework_TestCase $this->assertEquals('', normalize_spaces('')); $this->assertEquals(null, normalize_spaces(null)); } + + /** + * Test arrays_combine + */ + public function testArraysCombination() + { + $arr = [['ab', 'cd'], ['ef', 'gh'], ['ij', 'kl'], ['m']]; + $expected = [ + 'abefijm', + 'cdefijm', + 'abghijm', + 'cdghijm', + 'abefklm', + 'cdefklm', + 'abghklm', + 'cdghklm', + ]; + $this->assertEquals($expected, arrays_combination($arr)); + } + } -- cgit v1.2.3 From 52b503105d389d1796698114573ff618b2ad34a2 Mon Sep 17 00:00:00 2001 From: ArthurHoaro <arthur@hoa.ro> Date: Sat, 7 Jan 2017 14:30:42 +0100 Subject: Improve datetime display Use php-intl extension to display datetimes a bit more nicely, depending on the locale. What changes: * the day is no longer displayed * day number and month are ordered according to the locale * the timezone is more readable (UTC+1 instead of CET) --- application/Utils.php | 72 +++++++++++++++++++++++++++----------- tests/UtilsTest.php | 46 ++++++++++++++++++------ tests/languages/bootstrap.php | 7 ++++ tests/languages/de/UtilsDeTest.php | 25 +++++++++++++ tests/languages/en/UtilsEnTest.php | 25 +++++++++++++ tests/languages/fr/UtilsFrTest.php | 25 +++++++++++++ tpl/default/linklist.html | 5 +-- tpl/vintage/linklist.html | 4 +-- 8 files changed, 174 insertions(+), 35 deletions(-) create mode 100644 tests/languages/bootstrap.php create mode 100644 tests/languages/de/UtilsDeTest.php create mode 100644 tests/languages/en/UtilsEnTest.php create mode 100644 tests/languages/fr/UtilsFrTest.php diff --git a/application/Utils.php b/application/Utils.php index 19fb7116..a936b09f 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -225,44 +225,46 @@ function autoLocale($headerLocale) $encodings = ['utf8', 'UTF-8']; if (!empty($matches[2])) { $second = [strtoupper($matches[2]), strtolower($matches[2])]; - $attempts = arrays_combination([$first, $separators, $second, ['.'], $encodings]); + $attempts = cartesian_product_generator([$first, $separators, $second, ['.'], $encodings]); } else { - $attempts = arrays_combination([$first, $separators, $first, ['.'], $encodings]); + $attempts = cartesian_product_generator([$first, $separators, $first, ['.'], $encodings]); } } } - setlocale(LC_ALL, $attempts); + setlocale(LC_ALL, implode('implode', iterator_to_array($attempts))); } /** - * Combine multiple arrays of string to get all possible strings. - * The order is important because this doesn't shuffle the entries. + * Build a Generator object representing the cartesian product from given $items. * * Example: * [['a'], ['b', 'c']] * will generate: - * - ab - * - ac - * - * TODO PHP 5.6: use the `...` operator instead of an array of array. + * [ + * ['a', 'b'], + * ['a', 'c'], + * ] * * @param array $items array of array of string * - * @return array Combined string from the input array. + * @return Generator representing the cartesian product of given array. + * + * @see https://en.wikipedia.org/wiki/Cartesian_product */ -function arrays_combination($items) +function cartesian_product_generator($items) { - $out = ['']; - foreach ($items as $item) { - $add = []; - foreach ($item as $element) { - foreach ($out as $key => $existingEntry) { - $add[] = $existingEntry . $element; - } + if (empty($items)) { + yield []; + } + $subArray = array_pop($items); + if (empty($subArray)) { + return; + } + foreach (cartesian_product_generator($items) as $item) { + foreach ($subArray as $value) { + yield $item + [count($item) => $value]; } - $out = $add; } - return $out; } /** @@ -303,3 +305,33 @@ function normalize_spaces($string) { return preg_replace('/\s{2,}/', ' ', trim($string)); } + +/** + * Format the date according to the locale. + * + * Requires php-intl to display international datetimes, + * otherwise default format '%c' will be returned. + * + * @param DateTime $date to format. + * @param bool $intl Use international format if true. + * + * @return bool|string Formatted date, or false if the input is invalid. + */ +function format_date($date, $intl = true) +{ + if (! $date instanceof DateTime) { + return false; + } + + if (! $intl || ! class_exists('IntlDateFormatter')) { + return strftime('%c', $date->getTimestamp()); + } + + $formatter = new IntlDateFormatter( + setlocale(LC_TIME, 0), + IntlDateFormatter::LONG, + IntlDateFormatter::LONG + ); + + return $formatter->format($date); +} diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index b8f608b9..e70cc1ae 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php @@ -23,7 +23,12 @@ class UtilsTest extends PHPUnit_Framework_TestCase // Expected log date format protected static $dateFormat = 'Y/m/d H:i:s'; - + + /** + * @var string Save the current timezone. + */ + protected static $defaultTimeZone; + /** * Assign reference data @@ -31,6 +36,17 @@ class UtilsTest extends PHPUnit_Framework_TestCase public static function setUpBeforeClass() { self::$sidHashes = ReferenceSessionIdHashes::getHashes(); + self::$defaultTimeZone = date_default_timezone_get(); + // Timezone without DST for test consistency + date_default_timezone_set('Africa/Nairobi'); + } + + /** + * Reset the timezone + */ + public static function tearDownAfterClass() + { + date_default_timezone_set(self::$defaultTimeZone); } /** @@ -286,20 +302,28 @@ class UtilsTest extends PHPUnit_Framework_TestCase /** * Test arrays_combine */ - public function testArraysCombination() + public function testCartesianProductGenerator() { $arr = [['ab', 'cd'], ['ef', 'gh'], ['ij', 'kl'], ['m']]; $expected = [ - 'abefijm', - 'cdefijm', - 'abghijm', - 'cdghijm', - 'abefklm', - 'cdefklm', - 'abghklm', - 'cdghklm', + ['ab', 'ef', 'ij', 'm'], + ['ab', 'ef', 'kl', 'm'], + ['ab', 'gh', 'ij', 'm'], + ['ab', 'gh', 'kl', 'm'], + ['cd', 'ef', 'ij', 'm'], + ['cd', 'ef', 'kl', 'm'], + ['cd', 'gh', 'ij', 'm'], + ['cd', 'gh', 'kl', 'm'], ]; - $this->assertEquals($expected, arrays_combination($arr)); + $this->assertEquals($expected, iterator_to_array(cartesian_product_generator($arr))); } + /** + * Test date_format() with invalid parameter. + */ + public function testDateFormatInvalid() + { + $this->assertFalse(format_date([])); + $this->assertFalse(format_date(null)); + } } diff --git a/tests/languages/bootstrap.php b/tests/languages/bootstrap.php new file mode 100644 index 00000000..95609210 --- /dev/null +++ b/tests/languages/bootstrap.php @@ -0,0 +1,7 @@ +<?php +if (! empty('UT_LOCALE')) { + setlocale(LC_ALL, getenv('UT_LOCALE')); +} + +require_once 'vendor/autoload.php'; + diff --git a/tests/languages/de/UtilsDeTest.php b/tests/languages/de/UtilsDeTest.php new file mode 100644 index 00000000..8a740389 --- /dev/null +++ b/tests/languages/de/UtilsDeTest.php @@ -0,0 +1,25 @@ +<?php + +require_once 'tests/UtilsTest.php'; + + +class UtilsDeTest extends UtilsTest +{ + /** + * Test date_format(). + */ + public function testDateFormat() + { + $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); + $this->assertRegExp('/1. Januar 2017 (um )?10:11:12 GMT\+0?3(:00)?/', format_date($date, true)); + } + + /** + * Test date_format() using builtin PHP function strftime. + */ + public function testDateFormatDefault() + { + $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); + $this->assertEquals('So 01 Jan 2017 10:11:12 EAT', format_date($date, false)); + } +} diff --git a/tests/languages/en/UtilsEnTest.php b/tests/languages/en/UtilsEnTest.php new file mode 100644 index 00000000..60bcb653 --- /dev/null +++ b/tests/languages/en/UtilsEnTest.php @@ -0,0 +1,25 @@ +<?php + +require_once 'tests/UtilsTest.php'; + + +class UtilsEnTest extends UtilsTest +{ + /** + * Test date_format(). + */ + public function testDateFormat() + { + $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); + $this->assertRegExp('/January 1, 2017 (at )?10:11:12 AM GMT\+0?3(:00)?/', format_date($date, true)); + } + + /** + * Test date_format() using builtin PHP function strftime. + */ + public function testDateFormatDefault() + { + $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); + $this->assertEquals('Sun 01 Jan 2017 10:11:12 AM EAT', format_date($date, false)); + } +} diff --git a/tests/languages/fr/UtilsFrTest.php b/tests/languages/fr/UtilsFrTest.php new file mode 100644 index 00000000..890308d3 --- /dev/null +++ b/tests/languages/fr/UtilsFrTest.php @@ -0,0 +1,25 @@ +<?php + +require_once 'tests/UtilsTest.php'; + + +class UtilsFrTest extends UtilsTest +{ + /** + * Test date_format(). + */ + public function testDateFormat() + { + $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); + $this->assertRegExp('/1 janvier 2017 (à )?10:11:12 UTC\+0?3(:00)?/', format_date($date)); + } + + /** + * Test date_format() using builtin PHP function strftime. + */ + public function testDateFormatDefault() + { + $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); + $this->assertEquals('dim. 01 janv. 2017 10:11:12 EAT', format_date($date, false)); + } +} diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html index a9712704..9bc3ba1a 100644 --- a/tpl/default/linklist.html +++ b/tpl/default/linklist.html @@ -171,10 +171,11 @@ <div class="linklist-item-infos-dateblock pure-u-lg-3-8 pure-u-1"> <a href="?{$value.shorturl}" title="{'Permalink'|t}"> {if="!$hide_timestamps || isLoggedIn()"} - {$updated=$value.updated_timestamp ? 'Edited: '. strftime('%c', $value.updated_timestamp) : 'Permalink'} + {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'} <span class="linkdate" title="{$updated}"> <i class="fa fa-clock-o"></i> - {function="strftime('%c', $value.timestamp)"}{if="$value.updated_timestamp"}*{/if} + {$value.created|format_date} + {if="$value.updated_timestamp"}*{/if} · </span> {/if} diff --git a/tpl/vintage/linklist.html b/tpl/vintage/linklist.html index 5accc92f..fc116667 100644 --- a/tpl/vintage/linklist.html +++ b/tpl/vintage/linklist.html @@ -99,11 +99,11 @@ <br> {if="$value.description"}<div class="linkdescription">{$value.description}</div>{/if} {if="!$hide_timestamps || isLoggedIn()"} - {$updated=$value.updated_timestamp ? 'Edited: '. strftime('%c', $value.updated_timestamp) : 'Permalink'} + {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'} <span class="linkdate" title="Permalink"> <a href="?{$value.shorturl}"> <span title="{$updated}"> - {function="strftime('%c', $value.timestamp)"} + {$value.created|format_date} {if="$value.updated_timestamp"}*{/if} </span> - permalink -- cgit v1.2.3 From 6c7d68645409cfad3858b5f252f5a49b148e3b53 Mon Sep 17 00:00:00 2001 From: ArthurHoaro <arthur@hoa.ro> Date: Sun, 15 Jan 2017 16:31:53 +0100 Subject: Run languages tests using PHPUnit test suites --- .travis.yml | 6 ++++++ Makefile | 16 ++++++++++++++-- composer.json | 3 ++- phpunit.xml | 20 ++++++++++++++++---- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 03071a47..2a5ff5e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,11 @@ sudo: false language: php +addons: + apt: + packages: + - locales + - language-pack-de + - language-pack-fr cache: directories: - $HOME/.composer/cache diff --git a/Makefile b/Makefile index f3065b77..1d8a73a2 100644 --- a/Makefile +++ b/Makefile @@ -124,8 +124,20 @@ test: @echo "-------" @echo "PHPUNIT" @echo "-------" - @mkdir -p sandbox - @$(BIN)/phpunit tests + @mkdir -p sandbox coverage + @$(BIN)/phpunit --coverage-php coverage/main.cov --testsuite unit-tests + +locale_test_%: + @UT_LOCALE=$*.utf8 \ + $(BIN)/phpunit \ + --coverage-php coverage/$(firstword $(subst _, ,$*)).cov \ + --bootstrap tests/languages/bootstrap.php \ + --testsuite language-$(firstword $(subst _, ,$*)) + +all_tests: test locale_test_de_DE locale_test_en_US locale_test_fr_FR + @$(BIN)/phpcov merge --html coverage coverage + @# --text doesn't work with phpunit 4.* (v5 requires PHP 5.6) + @#$(BIN)/phpcov merge --text coverage/txt coverage ## # Custom release archive generation diff --git a/composer.json b/composer.json index b82aceef..70b87bb9 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "phpmd/phpmd" : "@stable", "phpunit/phpunit": "4.8.*", "sebastian/phpcpd": "*", - "squizlabs/php_codesniffer": "2.*" + "squizlabs/php_codesniffer": "2.*", + "phpunit/phpcov": "*" }, "autoload": { "psr-4": { diff --git a/phpunit.xml b/phpunit.xml index d6e01c35..8b66e6c5 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -3,13 +3,25 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd" colors="true"> + <testsuites> + <testsuite name="unit-tests"> + <directory>tests</directory> + <exclude>tests/languages</exclude> + </testsuite> + <testsuite name="language-de"> + <directory>tests/languages/de</directory> + </testsuite> + <testsuite name="language-en"> + <directory>tests/languages/en</directory> + </testsuite> + <testsuite name="language-fr"> + <directory>tests/languages/fr</directory> + </testsuite> + </testsuites> + <filter> <whitelist addUncoveredFilesFromWhitelist="true"> <directory suffix=".php">application</directory> </whitelist> </filter> - <logging> - <log type="coverage-html" target="coverage" lowUpperBound="30" highLowerBound="80"/> - <log type="coverage-text" target="php://stdout" showUncoveredFiles="true"/> - </logging> </phpunit> -- cgit v1.2.3 From 36c8fb1ef869c29e783f0dd5ebef2fb5566e2611 Mon Sep 17 00:00:00 2001 From: ArthurHoaro <arthur@hoa.ro> Date: Mon, 6 Mar 2017 20:34:02 +0100 Subject: Use all_tests target in Travis CI --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2a5ff5e3..59b86c08 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,4 +20,4 @@ install: script: - make clean - make check_permissions - - make test + - make all_tests -- cgit v1.2.3