generated from jric11/baseProject
Initial commit
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* StepITest.php
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*
|
||||
* This file is part of tc-lib-unicode software library.
|
||||
*/
|
||||
|
||||
namespace Test\Bidi;
|
||||
|
||||
use Com\Tecnick\Unicode\Bidi\StepI;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Bidi Test
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*/
|
||||
class StepITest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @param array{
|
||||
* 'e': int,
|
||||
* 'edir': string,
|
||||
* 'end': int,
|
||||
* 'eos': string,
|
||||
* 'length': int,
|
||||
* 'maxlevel': int,
|
||||
* 'sos': string,
|
||||
* 'start': int,
|
||||
* 'item': array<int, array{
|
||||
* 'char': int,
|
||||
* 'i': int,
|
||||
* 'level': int,
|
||||
* 'otype': string,
|
||||
* 'pdimatch': int,
|
||||
* 'pos': int,
|
||||
* 'type': string,
|
||||
* 'x': int,
|
||||
* }>,
|
||||
* } $seq Isolated Sequence array
|
||||
*
|
||||
* @param mixed $expected Expected result
|
||||
*/
|
||||
#[DataProvider('stepIDataProvider')]
|
||||
public function testStepI(array $seq, mixed $expected): void
|
||||
{
|
||||
$stepi = new StepI($seq);
|
||||
$this->assertEquals($expected, $stepi->getSequence());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, array{
|
||||
* 'e': int,
|
||||
* 'edir': string,
|
||||
* 'end': int,
|
||||
* 'eos': string,
|
||||
* 'length': int,
|
||||
* 'maxlevel': int,
|
||||
* 'sos': string,
|
||||
* 'start': int,
|
||||
* 'item': array<int, array{
|
||||
* 'char': int,
|
||||
* 'i': int,
|
||||
* 'level': int,
|
||||
* 'otype': string,
|
||||
* 'pdimatch': int,
|
||||
* 'pos': int,
|
||||
* 'type': string,
|
||||
* 'x': int,
|
||||
* }>,
|
||||
* }>>
|
||||
*/
|
||||
public static function stepIDataProvider(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
[
|
||||
'e' => 0,
|
||||
'edir' => 'L',
|
||||
'end' => 3,
|
||||
'eos' => 'L',
|
||||
'length' => 4,
|
||||
'maxlevel' => 0,
|
||||
'sos' => 'L',
|
||||
'start' => 0,
|
||||
'item' => [
|
||||
[
|
||||
'char' => 65,
|
||||
'i' => -1,
|
||||
'level' => 0,
|
||||
'otype' => 'L',
|
||||
'pos' => 0,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'L',
|
||||
'x' => 0,
|
||||
],
|
||||
[
|
||||
'char' => 8207,
|
||||
'i' => -1,
|
||||
'level' => 0,
|
||||
'otype' => 'R',
|
||||
'pos' => 1,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'R',
|
||||
'x' => 0,
|
||||
],
|
||||
[
|
||||
'char' => 1632,
|
||||
'i' => -1,
|
||||
'level' => 0,
|
||||
'otype' => 'AN',
|
||||
'pos' => 2,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'AN',
|
||||
'x' => 0,
|
||||
],
|
||||
[
|
||||
'char' => 1776,
|
||||
'i' => -1,
|
||||
'level' => 0,
|
||||
'otype' => 'EN',
|
||||
'pos' => 3,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'EN',
|
||||
'x' => 0,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'e' => 0,
|
||||
'edir' => 'L',
|
||||
'end' => 3,
|
||||
'eos' => 'L',
|
||||
'length' => 4,
|
||||
'maxlevel' => 2,
|
||||
'sos' => 'L',
|
||||
'start' => 0,
|
||||
'item' => [
|
||||
[
|
||||
'char' => 65,
|
||||
'i' => -1,
|
||||
'level' => 0,
|
||||
'otype' => 'L',
|
||||
'pos' => 0,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'L',
|
||||
'x' => 0,
|
||||
],
|
||||
[
|
||||
'char' => 8207,
|
||||
'i' => -1,
|
||||
'level' => 1,
|
||||
'otype' => 'R',
|
||||
'pos' => 1,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'R',
|
||||
'x' => 0,
|
||||
],
|
||||
[
|
||||
'char' => 1632,
|
||||
'i' => -1,
|
||||
'level' => 2,
|
||||
'otype' => 'AN',
|
||||
'pos' => 2,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'AN',
|
||||
'x' => 0,
|
||||
],
|
||||
[
|
||||
'char' => 1776,
|
||||
'i' => -1,
|
||||
'level' => 2,
|
||||
'otype' => 'EN',
|
||||
'pos' => 3,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'EN',
|
||||
'x' => 0,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
'e' => 1,
|
||||
'edir' => 'R',
|
||||
'end' => 3,
|
||||
'eos' => 'R',
|
||||
'length' => 4,
|
||||
'maxlevel' => 0,
|
||||
'sos' => 'R',
|
||||
'start' => 0,
|
||||
'item' => [
|
||||
[
|
||||
'char' => 65,
|
||||
'i' => -1,
|
||||
'level' => 1,
|
||||
'otype' => 'L',
|
||||
'pos' => 0,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'L',
|
||||
'x' => 0,
|
||||
],
|
||||
[
|
||||
'char' => 8207,
|
||||
'i' => -1,
|
||||
'level' => 1,
|
||||
'otype' => 'R',
|
||||
'pos' => 1,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'R',
|
||||
'x' => 0,
|
||||
],
|
||||
[
|
||||
'char' => 1632,
|
||||
'i' => -1,
|
||||
'level' => 1,
|
||||
'otype' => 'AN',
|
||||
'pos' => 2,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'AN',
|
||||
'x' => 0,
|
||||
],
|
||||
[
|
||||
'char' => 1776,
|
||||
'i' => -1,
|
||||
'level' => 1,
|
||||
'otype' => 'EN',
|
||||
'pos' => 3,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'EN',
|
||||
'x' => 0,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'e' => 1,
|
||||
'edir' => 'R',
|
||||
'end' => 3,
|
||||
'eos' => 'R',
|
||||
'length' => 4,
|
||||
'maxlevel' => 2,
|
||||
'sos' => 'R',
|
||||
'start' => 0,
|
||||
'item' => [
|
||||
[
|
||||
'char' => 65,
|
||||
'i' => -1,
|
||||
'level' => 2,
|
||||
'otype' => 'L',
|
||||
'pos' => 0,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'L',
|
||||
'x' => 0,
|
||||
],
|
||||
[
|
||||
'char' => 8207,
|
||||
'i' => -1,
|
||||
'level' => 1,
|
||||
'otype' => 'R',
|
||||
'pos' => 1,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'R',
|
||||
'x' => 0,
|
||||
],
|
||||
[
|
||||
'char' => 1632,
|
||||
'i' => -1,
|
||||
'level' => 2,
|
||||
'otype' => 'AN',
|
||||
'pos' => 2,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'AN',
|
||||
'x' => 0,
|
||||
],
|
||||
[
|
||||
'char' => 1776,
|
||||
'i' => -1,
|
||||
'level' => 2,
|
||||
'otype' => 'EN',
|
||||
'pos' => 3,
|
||||
'pdimatch' => -1,
|
||||
'type' => 'EN',
|
||||
'x' => 0,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* BidiConformanceTest.php
|
||||
*
|
||||
* @since 2026-08-06
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*
|
||||
* This file is part of tc-lib-unicode software library.
|
||||
*/
|
||||
|
||||
namespace Test;
|
||||
|
||||
use Com\Tecnick\Unicode\Bidi;
|
||||
use Com\Tecnick\Unicode\Data\Mirror as UniMirror;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Runs the official BidiCharacterTest.txt conformance suite of the Unicode Character
|
||||
* Database against the bidirectional algorithm.
|
||||
*
|
||||
* The data file is downloaded by "make ucd" into target/ucd/<version>/; the test is
|
||||
* skipped when it is not available.
|
||||
*
|
||||
* @since 2026-08-06
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*/
|
||||
class BidiConformanceTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Paragraph direction of field 1 of the data file.
|
||||
*/
|
||||
private const DIRECTION = [
|
||||
'0' => 'L',
|
||||
'1' => 'R',
|
||||
'2' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* Known failures, as "<field 0>;<field 1>" of the data file, with the reason.
|
||||
* A case listed here must fail: the test reports the entries that can be removed.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private const KNOWN_FAILURES = [];
|
||||
|
||||
/**
|
||||
* Maximum number of unexpected failures reported by the assertion message.
|
||||
*/
|
||||
private const MAX_REPORTED = 10;
|
||||
|
||||
public function testBidiCharacterTest(): void
|
||||
{
|
||||
$path = self::findDataFile();
|
||||
if ($path === null) {
|
||||
$this->markTestSkipped('BidiCharacterTest.txt not found: run "make ucd" to download it');
|
||||
}
|
||||
|
||||
$handle = \fopen($path, 'r');
|
||||
$this->assertIsResource($handle);
|
||||
|
||||
$total = 0;
|
||||
$failures = [];
|
||||
$fixed = [];
|
||||
|
||||
while (($line = \fgets($handle)) !== false) {
|
||||
$line = \trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$field = \explode(';', $line);
|
||||
if (\count($field) < 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
++$total;
|
||||
$key = $field[0] . ';' . ($field[1] ?? '');
|
||||
$passed = self::runCase($field);
|
||||
|
||||
if (isset(self::KNOWN_FAILURES[$key])) {
|
||||
if ($passed) {
|
||||
$fixed[] = $key;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$passed) {
|
||||
$failures[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
\fclose($handle);
|
||||
|
||||
$this->assertGreaterThan(90_000, $total, 'unexpected number of conformance cases');
|
||||
$this->assertSame([], $fixed, 'these cases now pass and must be removed from KNOWN_FAILURES');
|
||||
$this->assertSame(
|
||||
[],
|
||||
\array_slice($failures, 0, self::MAX_REPORTED),
|
||||
\sprintf('%d of %d conformance cases failed', \count($failures), $total),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a single data file line: codepoints, paragraph direction, paragraph level,
|
||||
* resolved levels and visual order.
|
||||
*
|
||||
* @param array<int, string> $field Fields of the data file line
|
||||
*/
|
||||
private static function runCase(array $field): bool
|
||||
{
|
||||
$codes = \trim($field[0] ?? '');
|
||||
$direction = \trim($field[1] ?? '');
|
||||
$levelStr = \trim($field[3] ?? '');
|
||||
$orderStr = \trim($field[4] ?? '');
|
||||
|
||||
$ordarr = \array_map(static fn(string $hex): int => (int) \hexdec($hex), \explode(' ', $codes));
|
||||
$levels = \explode(' ', $levelStr);
|
||||
$order = $orderStr === '' ? [] : \array_map('intval', \explode(' ', $orderStr));
|
||||
|
||||
// The data file lists the reordered indexes without applying rule L4, which the
|
||||
// library does apply, so the mirrored form is used for the odd levels.
|
||||
$expected = [];
|
||||
foreach ($order as $idx) {
|
||||
$ord = $ordarr[$idx] ?? 0;
|
||||
$expected[] = ((int) ($levels[$idx] ?? 0) % 2) === 1 ? UniMirror::UNI[$ord] ?? $ord : $ord;
|
||||
}
|
||||
|
||||
try {
|
||||
$bidi = new Bidi(null, null, $ordarr, self::DIRECTION[$direction] ?? '', false);
|
||||
$result = \array_values($bidi->getOrdArray());
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $result === $expected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path of the most recent BidiCharacterTest.txt available, or null.
|
||||
*/
|
||||
private static function findDataFile(): ?string
|
||||
{
|
||||
$found = \glob(__DIR__ . '/../target/ucd/*/BidiCharacterTest.txt');
|
||||
if ($found === false || $found === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
\sort($found);
|
||||
|
||||
return \end($found);
|
||||
}
|
||||
}
|
||||
+549
@@ -0,0 +1,549 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* BidiTest.php
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*
|
||||
* This file is part of tc-lib-unicode software library.
|
||||
*/
|
||||
|
||||
namespace Test;
|
||||
|
||||
use Com\Tecnick\Unicode\Bidi;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
/**
|
||||
* Bidi Test
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*/
|
||||
class BidiTest extends TestUtil
|
||||
{
|
||||
private static function decodeJsonString(string $json): string
|
||||
{
|
||||
/** @var string */
|
||||
return \json_decode($json);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testException(): void
|
||||
{
|
||||
$this->bcExpectException(\Com\Tecnick\Unicode\Exception::class);
|
||||
new \Com\Tecnick\Unicode\Bidi();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $str String to convert (if null it will be generated from $chrarr or $ordarr)
|
||||
* @param ?array<string> $chrarr Array of UTF-8 chars (if empty it will be generated from $str or $ordarr)
|
||||
* @param ?array<int> $ordarr Array of UTF-8 codepoints (if empty it will be generated from $str or $chrarr)
|
||||
* @param string $forcedir If 'R' forces RTL, if 'L' forces LTR
|
||||
* @param bool $shaping If true enable the shaping algorithm
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
#[DataProvider('inputDataProvider')]
|
||||
public function testStr(
|
||||
?string $str = null,
|
||||
?array $chrarr = null,
|
||||
?array $ordarr = null,
|
||||
string $forcedir = '',
|
||||
bool $shaping = true,
|
||||
): void {
|
||||
$bidi = new Bidi($str, $chrarr, $ordarr, $forcedir, $shaping);
|
||||
$this->assertEquals('test', $bidi->getString());
|
||||
$this->assertEquals(['t', 'e', 's', 't'], $bidi->getChrArray());
|
||||
$this->assertEquals([116, 101, 115, 116], $bidi->getOrdArray());
|
||||
$this->assertEquals(
|
||||
[
|
||||
116 => true,
|
||||
101 => true,
|
||||
115 => true,
|
||||
],
|
||||
$bidi->getCharKeys(),
|
||||
);
|
||||
$this->assertEquals(4, $bidi->getNumChars());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{?string, ?array<string>, ?array<int>, string, bool}>
|
||||
*/
|
||||
public static function inputDataProvider(): array
|
||||
{
|
||||
return [
|
||||
['test', null, null, '', true],
|
||||
[null, ['t', 'e', 's', 't'], null, '', true],
|
||||
[null, null, [116, 101, 115, 116], '', true],
|
||||
['test', ['t', 'e', 's', 't'], null, '', true],
|
||||
['test', null, [116, 101, 115, 116], '', true],
|
||||
[null, ['t', 'e', 's', 't'], [116, 101, 115, 116], '', true],
|
||||
['test', ['t', 'e', 's', 't'], [116, 101, 115, 116], '', true],
|
||||
['test', null, null, 'L', true],
|
||||
['test', null, null, 'R', true],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
#[DataProvider('bidiStrDataProvider')]
|
||||
public function testBidiStr(string $str, mixed $expected, string $forcedir = ''): void
|
||||
{
|
||||
$bidi = new Bidi($str, null, null, $forcedir, true);
|
||||
$this->assertEquals($expected, $bidi->getString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{string, string, string}>
|
||||
*/
|
||||
public static function bidiStrDataProvider(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
"\n\nABC\nEFG\n\nHIJ\n\n",
|
||||
"\n\nABC\nEFG\n\nHIJ\n\n",
|
||||
'L',
|
||||
],
|
||||
[
|
||||
self::decodeJsonString('"\u202EABC\u202C"'),
|
||||
'CBA',
|
||||
'',
|
||||
],
|
||||
[
|
||||
// All-L text in a forced-RTL paragraph: N1 resolves the spaces to L,
|
||||
// so the whole phrase is a single LTR run and keeps its word order.
|
||||
'left to right',
|
||||
'left to right',
|
||||
'R',
|
||||
],
|
||||
[
|
||||
// Same, but L1.4 resets the trailing space to the paragraph level,
|
||||
// moving it to the visual left.
|
||||
'left to right ',
|
||||
' left to right',
|
||||
'R',
|
||||
],
|
||||
[
|
||||
// U+0600..U+0602 are Arabic numbers (AN): I1 puts them on an even level,
|
||||
// so they keep their logical order inside the right-to-left paragraph.
|
||||
self::decodeJsonString('"smith (fabrikam \u0600\u0601\u0602) \u05de\u05d6\u05dc"'),
|
||||
self::decodeJsonString('"\u05dc\u05d6\u05de (\u0600\u0601\u0602 fabrikam) smith"'),
|
||||
'R',
|
||||
],
|
||||
[
|
||||
self::decodeJsonString('"\u0600\u0601\u0602 book(s)"'),
|
||||
self::decodeJsonString('"book(s) \u0600\u0601\u0602"'),
|
||||
'R',
|
||||
],
|
||||
[
|
||||
self::decodeJsonString('"\u0600\u0601(\u0602\u0603[&ef]!)gh"'),
|
||||
self::decodeJsonString('"gh(![ef&]\u0602\u0603)\u0600\u0601"'),
|
||||
'R',
|
||||
],
|
||||
[
|
||||
'تشكيل اختبار',
|
||||
'ﺭﺎﺒﺘﺧﺍ ﻞﻴﻜﺸﺗ',
|
||||
'',
|
||||
],
|
||||
[
|
||||
self::decodeJsonString('"\u05de\u05d6\u05dc \u05d8\u05d5\u05d1"'),
|
||||
self::decodeJsonString('"\u05d1\u05d5\u05d8 \u05dc\u05d6\u05de"'),
|
||||
'',
|
||||
],
|
||||
[
|
||||
self::decodeJsonString(
|
||||
'"\u0644\u0644\u0647 \u0600\u0601\u0602 \uFB50'
|
||||
. ' \u0651\u064c\u0651\u064d\u0651\u064e\u0651\u064f\u0651\u0650'
|
||||
. ' \u0644\u0622"',
|
||||
),
|
||||
// "\u0644\u0644\u0647" is not the word Allah: without the alef it shapes as
|
||||
// lam initial, lam medial and heh final instead of the U+FDF2 ligature.
|
||||
self::decodeJsonString(
|
||||
'"\ufef5 \ufc62\ufc61\ufc60\ufc5f\ufc5e \ufb50 \u0600\u0601\u0602 \ufeea\ufee0\ufedf"',
|
||||
),
|
||||
'',
|
||||
],
|
||||
[
|
||||
self::decodeJsonString('"A\u2067\u05d8\u2069B"'),
|
||||
self::decodeJsonString('"A\u2067\u05d8\u2069B"'),
|
||||
'',
|
||||
],
|
||||
[
|
||||
// Unterminated isolate: RLI with no matching PDI. Exercises StepXten's
|
||||
// findMatchingPdiStart() returning -1 and the eos-from-paragraph-level fallback.
|
||||
self::decodeJsonString('"\u05d0\u2067\u05d1"'),
|
||||
self::decodeJsonString('"\u05d1\u2067\u05d0"'),
|
||||
'',
|
||||
],
|
||||
[
|
||||
// Unterminated FSI wrapping LTR text: covers FSI auto-direction with no matching PDI.
|
||||
self::decodeJsonString('"\u05d0\u2068ab\u05d1"'),
|
||||
self::decodeJsonString('"ab\u05d1\u2068\u05d0"'),
|
||||
'',
|
||||
],
|
||||
[
|
||||
// RLI + PDI
|
||||
self::decodeJsonString('"The words \"\u2067\u05de\u05d6\u05dc [mazel] \u05d8\u05d5\u05d1 [tov]\u2069\"'
|
||||
. ' mean \"Congratulations!\""'),
|
||||
'The words "[tov] בוט [mazel] לזמ" mean "Congratulations!"',
|
||||
'',
|
||||
],
|
||||
[
|
||||
// RLE + PDF. The all-L phrase inside the embedding stays one LTR run,
|
||||
// and the legacy embedding spills over: sos of the run after PDF is R, so N1
|
||||
// resolves '" - ' between it and the number to R, pulling '" - $19.95' into
|
||||
// the RTL context (the spillover problem that isolates were made to solve).
|
||||
self::decodeJsonString('"it is called \"\u202bAN INTRODUCTION TO java\u202c\" - $19.95 in hardcover."'),
|
||||
'it is called "$19.95 - "AN INTRODUCTION TO java in hardcover.',
|
||||
'',
|
||||
],
|
||||
[
|
||||
// RLI + PDI: the isolate shields the surrounding text, so unlike the RLE
|
||||
// case above the quote and price stay in place and the phrase keeps its
|
||||
// word order.
|
||||
self::decodeJsonString('"it is called \"\u2067AN INTRODUCTION TO java\u2069\" - $19.95 in hardcover."'),
|
||||
'it is called "AN INTRODUCTION TO java" - $19.95 in hardcover.',
|
||||
'',
|
||||
],
|
||||
[
|
||||
// Hebrew with embedded paragraph separator (covers getParagraphs() splitting and re-insertion)
|
||||
self::decodeJsonString('"\u05de\u05d6\u05dc \u05d8\u05d5\u05d1"')
|
||||
. "\n"
|
||||
. self::decodeJsonString('"\u05de\u05d6\u05dc \u05d8\u05d5\u05d1"'),
|
||||
self::decodeJsonString('"\u05d1\u05d5\u05d8 \u05dc\u05d6\u05de"')
|
||||
. "\n"
|
||||
. self::decodeJsonString('"\u05d1\u05d5\u05d8 \u05dc\u05d6\u05de"'),
|
||||
'',
|
||||
],
|
||||
[
|
||||
// Hebrew ending with paragraph separator (covers empty last paragraph handling)
|
||||
self::decodeJsonString('"\u05de\u05d6\u05dc \u05d8\u05d5\u05d1"') . "\n",
|
||||
self::decodeJsonString('"\u05d1\u05d5\u05d8 \u05dc\u05d6\u05de"') . "\n",
|
||||
'',
|
||||
],
|
||||
[
|
||||
// Arabic with forced LTR direction (covers getPel() returning 0 for forcedir='L').
|
||||
// N1 resolves the space between the two R words, so the phrase is a single RTL
|
||||
// run inside the LTR paragraph and is reversed as a unit (words swap places).
|
||||
'تشكيل اختبار',
|
||||
self::decodeJsonString('"\ufead\ufe8e\ufe92\ufe98\ufea7\ufe8d\u0020\ufede\ufef4\ufedc\ufeb8\ufe97"'),
|
||||
'L',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression test for https://github.com/tecnickcom/tc-lib-unicode/issues/12
|
||||
* Arabic shaping must replace a lam-alef pair with a single ligature glyph
|
||||
* without deleting any other character of the run.
|
||||
*
|
||||
* @param array<int> $expected Shaped codepoints in visual order
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
#[DataProvider('lamAlefShapingDataProvider')]
|
||||
public function testLamAlefShaping(string $str, array $expected): void
|
||||
{
|
||||
$bidi = new Bidi($str);
|
||||
$this->assertSame($expected, $bidi->getOrdArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{string, array<int>}>
|
||||
*/
|
||||
public static function lamAlefShapingDataProvider(): array
|
||||
{
|
||||
return [
|
||||
// khah + lam + alef + lam: the pair merges and the khah keeps its initial form
|
||||
['خلال', [0xFEDD, 0xFEFC, 0xFEA7]],
|
||||
// nine letters with one lam-alef pair: one glyph less, leading alef preserved
|
||||
['الاستخدام', [0xFEE1, 0xFE8D, 0xFEAA, 0xFEA8, 0xFE98, 0xFEB3, 0xFEFB, 0xFE8D]],
|
||||
// two adjacent lam-alef pairs merge independently
|
||||
['لالا', [0xFEFB, 0xFEFB]],
|
||||
// both words shape identically (the second merge must not delete the first char)
|
||||
['خلال خلال', [0xFEDD, 0xFEFC, 0xFEA7, 0x0020, 0xFEDD, 0xFEFC, 0xFEA7]],
|
||||
// NSM between the pair: the ligature still forms around the shadda
|
||||
['لّا', [0xFEFB, 0x0651]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The Arabic shaper picks the presentation form from the Joining_Type of the
|
||||
* surrounding characters: transparent marks are skipped, ZWJ joins, ZWNJ and every
|
||||
* non-joining character break the connection.
|
||||
*
|
||||
* @param array<int> $expected Shaped codepoints in visual order
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
#[DataProvider('joiningShapingDataProvider')]
|
||||
public function testJoiningShaping(string $str, array $expected): void
|
||||
{
|
||||
$bidi = new Bidi($str);
|
||||
$this->assertSame($expected, $bidi->getOrdArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{string, array<int>}>
|
||||
*/
|
||||
public static function joiningShapingDataProvider(): array
|
||||
{
|
||||
return [
|
||||
// ZWNJ (U+200C) breaks the connection between two dual-joining letters
|
||||
'beh ZWNJ teh' => ["\u{0628}\u{200C}\u{062A}", [0xFE95, 0xFE8F]],
|
||||
// and it also blocks the lam-alef ligature
|
||||
'lam ZWNJ alef' => ["\u{0644}\u{200C}\u{0627}", [0xFE8D, 0xFEDD]],
|
||||
// ZWJ (U+200D) is join causing: the beh takes its initial form
|
||||
'beh ZWJ' => ["\u{0628}\u{200D}", [0xFE91]],
|
||||
// Persian needs ZWNJ inside a word
|
||||
'persian mikhaham' => [
|
||||
'میخواهم',
|
||||
[0xFEE2, 0xFEEB, 0xFE8D, 0xFEEE, 0xFEA7, 0xFBFD, 0xFEE3],
|
||||
],
|
||||
// a non-joining character between two letters isolates both
|
||||
'beh comma teh' => ['ب،ت', [0xFE95, 0x060C, 0xFE8F]],
|
||||
'beh digit teh' => ['ب١ت', [0xFE95, 0x0661, 0xFE8F]],
|
||||
// letters with an isolated and a final form only
|
||||
'waw with hamza' => ['سؤال', [0xFEDD, 0xFE8D, 0xFE86, 0xFEB3]],
|
||||
'teh marbuta' => ['بةب', [0xFE8F, 0xFE94, 0xFE91]],
|
||||
// right joining letters do not connect to the following letter
|
||||
'urdu rreh' => ['بڑا', [0xFE8D, 0xFB8D, 0xFE91]],
|
||||
'alef wasla' => ['ٱلحمد', [0xFEAA, 0xFEE4, 0xFEA4, 0xFEDF, 0xFB50]],
|
||||
'uyghur' => ['ئۇيغۇر', [0xFEAD, 0xFBD8, 0xFED0, 0xFEF3, 0xFBD8, 0xFE8B]],
|
||||
// non-joining letters and punctuation are not a joining context
|
||||
'hamza' => ['شيء', [0xFE80, 0xFEF2, 0xFEB7]],
|
||||
'arabic semicolon' => ['ب؛', [0x061B, 0xFE8F]],
|
||||
'urdu full stop' => ['اب۔', [0x06D4, 0xFE8F, 0xFE8D]],
|
||||
'arabic question mark' => ['ب؟', [0x061F, 0xFE8F]],
|
||||
'two beh and question mark' => ['بب؟', [0x061F, 0xFE90, 0xFE91]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Shadda (U+0651) and a second mark are merged into a single glyph in both orders:
|
||||
* canonical ordering puts the vowel first, as its combining class is lower.
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testCombineShadda(): void
|
||||
{
|
||||
// BEH + SHADDA + FATHA and BEH + FATHA + SHADDA
|
||||
$bidi = new Bidi(null, null, [0x0628, 0x0651, 0x064E]);
|
||||
$this->assertSame([0xFC60, 0xFE8F], $bidi->getOrdArray());
|
||||
|
||||
$bidi = new Bidi(null, null, [0x0628, 0x064E, 0x0651]);
|
||||
$this->assertSame([0xFC60, 0xFE8F], $bidi->getOrdArray());
|
||||
|
||||
// BEH + SHADDA + SUPERSCRIPT ALEF
|
||||
$bidi = new Bidi(null, null, [0x0628, 0x0651, 0x0670]);
|
||||
$this->assertSame([0xFC63, 0xFE8F], $bidi->getOrdArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* The U+FDF2 ligature covers alef + lam + lam + heh and has an isolated form only.
|
||||
*
|
||||
* @param array<int> $expected Shaped codepoints in visual order
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
#[DataProvider('allahShapingDataProvider')]
|
||||
public function testAllahShaping(string $str, array $expected): void
|
||||
{
|
||||
$bidi = new Bidi($str);
|
||||
$this->assertSame($expected, $bidi->getOrdArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{string, array<int>}>
|
||||
*/
|
||||
public static function allahShapingDataProvider(): array
|
||||
{
|
||||
return [
|
||||
// the four characters become one glyph
|
||||
'allah' => ['الله', [0xFDF2]],
|
||||
// the combining marks are transparent and are kept after the ligature
|
||||
'vocalized allah' => ['اللّٰه', [0xFDF2, 0xFC63]],
|
||||
// without the alef the word is shaped letter by letter
|
||||
'lillah' => ['لله', [0xFEEA, 0xFEE0, 0xFEDF]],
|
||||
'three lam and heh' => ['للله', [0xFEEA, 0xFEE0, 0xFEE0, 0xFEDF]],
|
||||
// the alef joins the beh, so the isolated ligature cannot be used
|
||||
'billah' => ['بالله', [0xFEEA, 0xFEE0, 0xFEDF, 0xFE8E, 0xFE91]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression test for https://github.com/tecnickcom/tc-lib-unicode/issues/13
|
||||
* Rules N1/N2 must resolve ordinary neutrals (WS, ON, S), not only the literal
|
||||
* 'NI'-typed isolate formatting characters: a neutral between two strong characters
|
||||
* of the same direction takes that direction, keeping multi-word runs together.
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
#[DataProvider('neutralResolutionDataProvider')]
|
||||
public function testNeutralResolution(string $str, string $expected, string $forcedir): void
|
||||
{
|
||||
$bidi = new Bidi($str, null, null, $forcedir, false);
|
||||
$this->assertSame($expected, $bidi->getString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{string, string, string}>
|
||||
*/
|
||||
public static function neutralResolutionDataProvider(): array
|
||||
{
|
||||
// The issue's third case (all-Latin text forced RTL) is covered by
|
||||
// bidiStrDataProvider: shaping is a no-op for ASCII input.
|
||||
return [
|
||||
// RTL paragraph with a two-word Latin phrase: the phrase keeps its word order
|
||||
['تجربة - John Doe', 'John Doe - ةبرجت', ''],
|
||||
// LTR paragraph with a two-word Arabic phrase: reversed as a single run
|
||||
['John Doe - تجربة خاصة', 'John Doe - ةصاخ ةبرجت', ''],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Bidi with edge-case ordarr inputs: a negative codepoint and a Private Use Area
|
||||
* codepoint as the last character of the paragraph.
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testBidiWithSpecialOrdarr(): void
|
||||
{
|
||||
// Negative codepoint as last char: covers the $lastchar < 0 branch
|
||||
$bidi1 = new \Com\Tecnick\Unicode\Bidi(null, null, [0x05D0, -1], 'R', false);
|
||||
$this->assertEquals([-1, 1488], $bidi1->getOrdArray());
|
||||
|
||||
// Codepoint 0xE001 (Private Use Area): type L, so it forms its own left-to-right run
|
||||
$bidi2 = new \Com\Tecnick\Unicode\Bidi(null, null, [0x05D0, 0xE001], 'R', false);
|
||||
$this->assertEquals([57345, 1488], $bidi2->getOrdArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* X9 removes the explicit formatting characters also when the text holds no
|
||||
* right-to-left character, so the left-to-right fast path cannot return them.
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testFormattingCharactersAreRemoved(): void
|
||||
{
|
||||
$bidi = new Bidi(self::decodeJsonString('"a\u202Ab\u202Cc"'));
|
||||
$this->assertSame('abc', $bidi->getString());
|
||||
|
||||
$bidi = new Bidi(self::decodeJsonString('"a\u2066b\u2069c"'));
|
||||
$this->assertSame(
|
||||
self::decodeJsonString('"a\u2066b\u2069c"'),
|
||||
$bidi->getString(),
|
||||
'the isolate formatting characters are retained',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The string, char array and codepoint array forms of the input must describe the
|
||||
* same text: a different number of characters is an input error.
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testMismatchedInputForms(): void
|
||||
{
|
||||
$this->bcExpectException(\Com\Tecnick\Unicode\Exception::class);
|
||||
new Bidi('test', null, [0x05D0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Conformance repros taken from the official BidiCharacterTest.txt suite, checked
|
||||
* against the codepoint sequence the UBA produces (shaping disabled).
|
||||
*
|
||||
* @param array<int> $ordarr Input codepoints
|
||||
* @param array<int> $expected Expected codepoints in visual order
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
#[DataProvider('ubaOrdArrDataProvider')]
|
||||
public function testUbaOrdArr(array $ordarr, string $forcedir, array $expected): void
|
||||
{
|
||||
$bidi = new Bidi(null, null, $ordarr, $forcedir, false);
|
||||
$this->assertSame($expected, \array_values($bidi->getOrdArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{array<int>, string, array<int>}>
|
||||
*/
|
||||
public static function ubaOrdArrDataProvider(): array
|
||||
{
|
||||
return [
|
||||
// X5c: the direction of an FSI comes from the content between it and its
|
||||
// matching PDI, so an FSI wrapping Hebrew text acts as an RLI.
|
||||
'FSI with RTL content' => [
|
||||
[0x2068, 0x05D0, 0x0021, 0x2069, 0x0061, 0x0062],
|
||||
'',
|
||||
[0x2068, 0x0021, 0x05D0, 0x2069, 0x0061, 0x0062],
|
||||
],
|
||||
// X5c with no matching PDI: the scan runs to the end of the paragraph.
|
||||
'unterminated FSI with RTL content' => [
|
||||
[0x2068, 0x05D0, 0x0061],
|
||||
'',
|
||||
[0x2068, 0x0061, 0x05D0],
|
||||
],
|
||||
// X6a: a PDI matching no isolate initiator keeps the level of the current stack
|
||||
// entry and is retained by X9 instead of being deleted.
|
||||
'unmatched PDI is retained' => [
|
||||
[0x05D0, 0x2069, 0x05D1],
|
||||
'',
|
||||
[0x05D1, 0x2069, 0x05D0],
|
||||
],
|
||||
// W4: a European separator only joins two European numbers, so the plus sign
|
||||
// between two Arabic-Indic digits stays a separator and N1 resolves it as R.
|
||||
'ES does not join two Arabic numbers' => [
|
||||
[0x0660, 0x002B, 0x0661],
|
||||
'R',
|
||||
[0x0661, 0x002B, 0x0660],
|
||||
],
|
||||
// Same rule with a hyphen between two European numbers resolved to AN by W2.
|
||||
'ES after an Arabic letter' => [
|
||||
[0x0028, 0x0627, 0x0029, 0x0020, 0x0031, 0x002D, 0x0032],
|
||||
'L',
|
||||
[0x0028, 0x0627, 0x0029, 0x0020, 0x0032, 0x002D, 0x0031],
|
||||
],
|
||||
// BD14/BD15: a bracket whose current type is not ON (here retyped by an
|
||||
// override) is not part of a bracket pair.
|
||||
'brackets retyped by an override do not pair' => [
|
||||
[0x202E, 0x0028, 0x202C, 0x202B, 0x05D0, 0x0062, 0x0029, 0x0063, 0x202C],
|
||||
'R',
|
||||
[0x0062, 0x0029, 0x0063, 0x05D0, 0x0029],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* BD16 uses a fixed 63 element stack: when a 64th opening bracket is found, bracket
|
||||
* pairing stops for the remainder of the isolating run sequence and the pairs found
|
||||
* so far are kept.
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testBracketStackLimit(): void
|
||||
{
|
||||
$ordarr = \array_merge([0x0061], \array_fill(0, 64, 0x0028), [0x0062], \array_fill(0, 64, 0x0029));
|
||||
|
||||
// None of the 64 pairs is resolved to L by N0, so N1 leaves the brackets at the
|
||||
// paragraph level: the whole line is reversed and every bracket is mirrored.
|
||||
$expected = \array_merge(\array_fill(0, 64, 0x0028), [0x0061], \array_fill(0, 64, 0x0028), [0x0062]);
|
||||
|
||||
$bidi = new Bidi(null, null, $ordarr, 'R', false);
|
||||
$this->assertSame($expected, \array_values($bidi->getOrdArray()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ConvertTest.php
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*
|
||||
* This file is part of tc-lib-unicode software library.
|
||||
*/
|
||||
|
||||
namespace Test;
|
||||
|
||||
use Com\Tecnick\Unicode\Data\Latin;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Convert Test
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*/
|
||||
class ConvertTest extends TestCase
|
||||
{
|
||||
protected function getTestObject(): \Com\Tecnick\Unicode\Convert
|
||||
{
|
||||
return new \Com\Tecnick\Unicode\Convert();
|
||||
}
|
||||
|
||||
private static function decodeJsonString(string $json): string
|
||||
{
|
||||
/** @var string */
|
||||
return \json_decode($json);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
#[DataProvider('chrDataProvider')]
|
||||
public function testChr(int $ord, mixed $expected): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$chr = $convert->chr($ord);
|
||||
$this->assertEquals($expected, $chr);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
#[DataProvider('chrDataProvider')]
|
||||
public function testOrd(mixed $expected, string $chr): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$ord = $convert->ord($chr);
|
||||
$this->assertEquals($expected, $ord);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{0:int,1:string}>
|
||||
*/
|
||||
public static function chrDataProvider(): array
|
||||
{
|
||||
return [
|
||||
[32, ' '],
|
||||
[48, '0'],
|
||||
[65, 'A'],
|
||||
[182, '¶'],
|
||||
[255, 'ÿ'],
|
||||
[256, 'Ā'],
|
||||
[544, 'Ƞ'],
|
||||
[916, 'Δ'],
|
||||
[1488, 'א'],
|
||||
[21488, '台'],
|
||||
[49436, '서'],
|
||||
[70039, '𑆗'],
|
||||
[195101, '𪘀'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testStrToChrArr(): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$res = $convert->strToChrArr('0A¶ÿĀȠΔא台서');
|
||||
$this->assertEquals(['0', 'A', '¶', 'ÿ', 'Ā', 'Ƞ', 'Δ', 'א', '台', '서'], $res);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testChrArrToOrdArr(): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$res = $convert->chrArrToOrdArr(['0', 'A', '¶', 'ÿ', 'Ā', 'Ƞ', 'Δ', 'א', '台', '서']);
|
||||
$this->assertEquals([48, 65, 182, 255, 256, 544, 916, 1488, 21488, 49436], $res);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testOrdArrToChrArr(): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$res = $convert->ordArrToChrArr([48, 65, 182, 255, 256, 544, 916, 1488, 21488, 49436]);
|
||||
$this->assertEquals(['0', 'A', '¶', 'ÿ', 'Ā', 'Ƞ', 'Δ', 'א', '台', '서'], $res);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testStrToOrdArr(): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$res = $convert->strToOrdArr('0A¶ÿĀȠΔא台서');
|
||||
$this->assertEquals([48, 65, 182, 255, 256, 544, 916, 1488, 21488, 49436], $res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty inputs must short-circuit to an empty array in every direction.
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testConvertArrEmpty(): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$this->assertSame([], $convert->strToOrdArr(''));
|
||||
$this->assertSame([], $convert->ordArrToChrArr([]));
|
||||
$this->assertSame([], $convert->chrArrToOrdArr([]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplementary-plane (4-byte) code points must round-trip through every conversion.
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testConvertArrSupplementaryPlane(): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
// Includes two supplementary-plane (4-byte UTF-8) code points: U+11197 and U+2A600.
|
||||
$str = "A\u{11197}\u{00B6}\u{2A600}\u{53F0}";
|
||||
$ords = [65, 70039, 182, 173568, 21488];
|
||||
$chrs = ['A', "\u{11197}", "\u{00B6}", "\u{2A600}", "\u{53F0}"];
|
||||
|
||||
$this->assertSame($ords, $convert->strToOrdArr($str));
|
||||
$this->assertSame($chrs, $convert->ordArrToChrArr($ords));
|
||||
$this->assertSame($ords, $convert->chrArrToOrdArr($chrs));
|
||||
$this->assertSame($str, \implode('', $convert->ordArrToChrArr($convert->strToOrdArr($str))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Malformed UTF-8 raises an exception, like strToChrArr(): a byte sequence that is not
|
||||
* valid UTF-8 is an input error, while an invalid code point is substituted with '?'.
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testStrToOrdArrRejectsMalformed(): void
|
||||
{
|
||||
$this->expectException(\Com\Tecnick\Unicode\Exception::class);
|
||||
$convert = $this->getTestObject();
|
||||
$convert->strToOrdArr("\xff\xfe");
|
||||
}
|
||||
|
||||
/**
|
||||
* Code points that cannot be encoded (negative, surrogate or above U+10FFFF) are
|
||||
* substituted with '?'.
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testOrdArrToChrArrSubstitutesInvalid(): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$this->assertSame(['?', '?', '?', 'A'], $convert->ordArrToChrArr([-1, 0xD800, 0x110000, 0x41]));
|
||||
}
|
||||
|
||||
public function testGetSubUniArrStr(): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$res = $convert->getSubUniArrStr(['0', 'A', '¶', 'ÿ', 'Ā', 'Ƞ', 'Δ', 'א', '台', '서']);
|
||||
$this->assertEquals('0A¶ÿĀȠΔא台서', $res);
|
||||
|
||||
$res = $convert->getSubUniArrStr(['0', 'A', '¶', 'ÿ', 'Ā', 'Ƞ', 'Δ', 'א', '台', '서'], 2, 8);
|
||||
$this->assertEquals('¶ÿĀȠΔא', $res);
|
||||
}
|
||||
|
||||
public function testUniArrToLatinArr(): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$uniarr = \array_keys(Latin::SUBSTITUTE);
|
||||
$uniarr[] = 65533; // 0xFFFD - character to ignore
|
||||
$uniarr[] = 123456; // undefined char
|
||||
$uniarr[] = 65; // ASCII char
|
||||
$latarr = \array_values(Latin::SUBSTITUTE);
|
||||
$latarr[] = 63;
|
||||
$latarr[] = 65;
|
||||
$res = $convert->uniArrToLatinArr($uniarr);
|
||||
$this->assertEquals($latarr, $res);
|
||||
}
|
||||
|
||||
public function testLatinArrToStr(): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$res = $convert->latinArrToStr([48, 57, 65, 90, 97, 122]);
|
||||
$this->assertEquals('09AZaz', $res);
|
||||
}
|
||||
|
||||
#[DataProvider('strToHexDataProvider')]
|
||||
public function testStrToHex(string $str, mixed $hex): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$res = $convert->strToHex($str);
|
||||
$this->assertEquals($hex, $res);
|
||||
}
|
||||
|
||||
#[DataProvider('strToHexDataProvider')]
|
||||
public function testHexToStr(mixed $str, string $hex): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$res = $convert->hexToStr($hex);
|
||||
$this->assertEquals($str, $res);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string>>
|
||||
*/
|
||||
public static function strToHexDataProvider(): array
|
||||
{
|
||||
return [
|
||||
['', ''],
|
||||
['A', '41'],
|
||||
['AB', '4142'],
|
||||
['ABC', '414243'],
|
||||
["\n", '0a'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('toUTF16BEDataProvider')]
|
||||
public function testToUTF16BE(string $str, mixed $exp): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$res = $convert->toUTF16BE($str);
|
||||
$this->assertEquals($exp, $convert->strToHex($res));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string>>
|
||||
*/
|
||||
public static function toUTF16BEDataProvider(): array
|
||||
{
|
||||
return [
|
||||
['', ''],
|
||||
['ABC', '004100420043'],
|
||||
[self::decodeJsonString('"\u0010\uffff\u00ff\uff00"'), '0010ffff00ffff00'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('toUTF8DataProvider')]
|
||||
public function testToUTF8(string $str, mixed $exp, ?string $enc = null): void
|
||||
{
|
||||
$convert = $this->getTestObject();
|
||||
$res = $convert->toUTF8($str, $enc);
|
||||
$this->assertEquals($exp, $res);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string>>
|
||||
*/
|
||||
public static function toUTF8DataProvider(): array
|
||||
{
|
||||
return [
|
||||
['', ''],
|
||||
['òèìòù', 'òèìòù'],
|
||||
['òèìòù', 'òèìòù', 'ISO-8859-1'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testToUTF8WithUndetectableEncoding(): void
|
||||
{
|
||||
// BASE64 and HTML-ENTITIES cannot detect a plain ASCII string,
|
||||
// causing mb_detect_encoding() to return false; the method must
|
||||
// handle this gracefully by falling back to a null source encoding.
|
||||
$convert = $this->getTestObject();
|
||||
$res = $convert->toUTF8('abc', ['BASE64', 'HTML-ENTITIES']);
|
||||
$this->assertEquals('abc', $res);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testOrdException(): void
|
||||
{
|
||||
$this->expectException(\Com\Tecnick\Unicode\Exception::class);
|
||||
$convert = $this->getTestObject();
|
||||
// An empty string produces a zero-length UCS-4BE buffer,
|
||||
// making unpack() fail and triggering the exception.
|
||||
$convert->ord('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testStrToChrArrException(): void
|
||||
{
|
||||
$this->expectException(\Com\Tecnick\Unicode\Exception::class);
|
||||
$convert = $this->getTestObject();
|
||||
// Invalid UTF-8 bytes cause preg_split() with the /u flag to return
|
||||
// false, triggering the exception.
|
||||
$convert->strToChrArr("\xff\xfe");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* DevanagariTest.php
|
||||
*
|
||||
* @since 2026-04-30
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*
|
||||
* This file is part of tc-lib-unicode software library.
|
||||
*/
|
||||
|
||||
namespace Test\Substitution;
|
||||
|
||||
use Com\Tecnick\Unicode\Substitution\Devanagari;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Test\TestUtil;
|
||||
|
||||
/**
|
||||
* Devanagari substitution test
|
||||
*
|
||||
* @since 2026-04-30
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*/
|
||||
class DevanagariTest extends TestUtil
|
||||
{
|
||||
/**
|
||||
* @param array<int, int> $input
|
||||
* @param array<int, int> $expected
|
||||
*/
|
||||
#[DataProvider('devanagariDataProvider')]
|
||||
public function testGetOrdarr(array $input, array $expected): void
|
||||
{
|
||||
$obj = new Devanagari($input);
|
||||
$this->assertSame($expected, $obj->getOrdarr());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{0: array<int, int>, 1: array<int, int>}>
|
||||
*/
|
||||
public static function devanagariDataProvider(): array
|
||||
{
|
||||
// Codepoint reference:
|
||||
// U+0915 KA U+0916 KHA U+0937 SHA U+0939 HA
|
||||
// U+094D VIRAMA U+093F VOWEL SIGN I (left matra)
|
||||
// U+0958 QA (first extended) U+095F YYA (last extended)
|
||||
|
||||
return [
|
||||
// Empty input returns empty output
|
||||
'empty' => [
|
||||
[],
|
||||
[],
|
||||
],
|
||||
|
||||
// Pure ASCII: no Devanagari codepoints, pass through unchanged
|
||||
'ascii_only' => [
|
||||
[0x41, 0x42, 0x43],
|
||||
[0x41, 0x42, 0x43],
|
||||
],
|
||||
|
||||
// Single consonant: no matra follows, pass through unchanged
|
||||
// U+0915 KA
|
||||
'consonant_only' => [
|
||||
[0x0915],
|
||||
[0x0915],
|
||||
],
|
||||
|
||||
// Last standard consonant: U+0939 HA
|
||||
'last_standard_consonant' => [
|
||||
[0x0939],
|
||||
[0x0939],
|
||||
],
|
||||
|
||||
// First extended consonant: U+0958 QA, unchanged (no matra)
|
||||
'first_extended_consonant' => [
|
||||
[0x0958],
|
||||
[0x0958],
|
||||
],
|
||||
|
||||
// Last extended consonant: U+095F YYA, unchanged (no matra)
|
||||
'last_extended_consonant' => [
|
||||
[0x095F],
|
||||
[0x095F],
|
||||
],
|
||||
|
||||
// Orphaned left matra at start (no preceding consonant): unchanged
|
||||
// U+093F alone
|
||||
'orphaned_left_matra' => [
|
||||
[0x093F],
|
||||
[0x093F],
|
||||
],
|
||||
|
||||
// Left matra followed by consonant (orphaned leading matra):
|
||||
// the matra is not recognised as following a cluster, unchanged
|
||||
// U+093F, U+0915
|
||||
'matra_then_consonant_no_reorder' => [
|
||||
[0x093F, 0x0915],
|
||||
[0x093F, 0x0915],
|
||||
],
|
||||
|
||||
// Simple reposition: U+0915 KA + U+093F → U+093F, U+0915
|
||||
'ka_with_i_vowel' => [
|
||||
[0x0915, 0x093F],
|
||||
[0x093F, 0x0915],
|
||||
],
|
||||
|
||||
// U+094E PRISHTHAMATRA E is the second vowel sign of Indic
|
||||
// Positional Category Left: U+0915 KA + U+094E
|
||||
'ka_with_prishthamatra_e' => [
|
||||
[0x0915, 0x094E],
|
||||
[0x094E, 0x0915],
|
||||
],
|
||||
|
||||
// Last standard consonant + left matra: U+0939 HA + U+093F
|
||||
'ha_with_i_vowel' => [
|
||||
[0x0939, 0x093F],
|
||||
[0x093F, 0x0939],
|
||||
],
|
||||
|
||||
// Extended consonant + left matra: U+0958 QA + U+093F
|
||||
'extended_consonant_with_matra' => [
|
||||
[0x0958, 0x093F],
|
||||
[0x093F, 0x0958],
|
||||
],
|
||||
|
||||
// Conjunct cluster: U+0915 KA + U+094D VIRAMA + U+0916 KHA + U+093F
|
||||
// → U+093F, U+0915, U+094D, U+0916
|
||||
'conjunct_with_matra' => [
|
||||
[0x0915, 0x094D, 0x0916, 0x093F],
|
||||
[0x093F, 0x0915, 0x094D, 0x0916],
|
||||
],
|
||||
|
||||
// Longer conjunct: KA + VIRAMA + SHA + VIRAMA + HA + VOWEL SIGN I
|
||||
// → U+093F, KA, VIRAMA, SHA, VIRAMA, HA
|
||||
'three_consonant_conjunct_with_matra' => [
|
||||
[0x0915, 0x094D, 0x0937, 0x094D, 0x0939, 0x093F],
|
||||
[0x093F, 0x0915, 0x094D, 0x0937, 0x094D, 0x0939],
|
||||
],
|
||||
|
||||
// Conjunct where virama is not followed by a consonant: cluster
|
||||
// ends at the virama; the matra after virama is NOT moved
|
||||
// U+0915, U+094D, U+093F → U+0915, U+094D, U+093F
|
||||
// (U+094D followed by non-consonant ends the cluster at KA only,
|
||||
// but then U+094D is the next codepoint: not a left matra)
|
||||
'virama_then_matra_no_reorder' => [
|
||||
[0x0915, 0x094D, 0x093F],
|
||||
[0x0915, 0x094D, 0x093F],
|
||||
],
|
||||
|
||||
// Consonant followed by non-matra: pass through unchanged
|
||||
// U+0915, 0x41 (ASCII A)
|
||||
'consonant_then_ascii' => [
|
||||
[0x0915, 0x41],
|
||||
[0x0915, 0x41],
|
||||
],
|
||||
|
||||
// Two separate simple clusters
|
||||
// KA+I, KHA+I → I+KA, I+KHA
|
||||
'two_simple_clusters' => [
|
||||
[0x0915, 0x093F, 0x0916, 0x093F],
|
||||
[0x093F, 0x0915, 0x093F, 0x0916],
|
||||
],
|
||||
|
||||
// Mixed: ASCII + Devanagari cluster + ASCII
|
||||
// 0x41, U+0915, U+093F, 0x42 → 0x41, U+093F, U+0915, 0x42
|
||||
'mixed_ascii_devanagari' => [
|
||||
[0x41, 0x0915, 0x093F, 0x42],
|
||||
[0x41, 0x093F, 0x0915, 0x42],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testNormalizesSparseIndexes(): void
|
||||
{
|
||||
$obj = new Devanagari([5 => 0x0915, 9 => 0x093F]);
|
||||
$this->assertSame([0x093F, 0x0915], $obj->getOrdarr());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* HangulTest.php
|
||||
*
|
||||
* @since 2026-04-30
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*
|
||||
* This file is part of tc-lib-unicode software library.
|
||||
*/
|
||||
|
||||
namespace Test\Substitution;
|
||||
|
||||
use Com\Tecnick\Unicode\Substitution\Hangul;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Test\TestUtil;
|
||||
|
||||
/**
|
||||
* Hangul Jamo composition test
|
||||
*
|
||||
* @since 2026-04-30
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*/
|
||||
class HangulTest extends TestUtil
|
||||
{
|
||||
/**
|
||||
* @param array<int, int> $input
|
||||
* @param array<int, int> $expected
|
||||
*/
|
||||
#[DataProvider('hangulDataProvider')]
|
||||
public function testGetOrdarr(array $input, array $expected): void
|
||||
{
|
||||
$obj = new Hangul($input);
|
||||
$this->assertSame($expected, $obj->getOrdarr());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{0: array<int, int>, 1: array<int, int>}>
|
||||
*
|
||||
* Expected values verified against Unicode conformance data (NormalizationTest.txt,
|
||||
* Hangul section) and the algorithmic formula in Unicode Standard 15.1 §3.12:
|
||||
* S = SBase(AC00) + (L−1100)×NCount(588) + (V−1161)×TCount(28) + (T−11A7)
|
||||
*
|
||||
* Spot-checks:
|
||||
* 가 (U+AC00) = AC00 + (1100−1100)×588 + (1161−1161)×28 + 0 = AC00
|
||||
* 나 (U+B098) = AC00 + (1102−1100)×588 + (1161−1161)×28 = AC00 + 2×588 = AC00+1176 = B1D8? No wait:
|
||||
* L=U+1102 (NIEUN), V=U+1161 (A)
|
||||
* S = AC00 + (1102-1100)*588 + (1161-1161)*28 = AC00 + 2*588 = AC00 + 1176 = 0xB1D8 -- that's 나 but wait
|
||||
* Actually 나 = U+B098: B098 - AC00 = 1176 - hmm, 0xB098 - 0xAC00 = 0x498 = 1176. Yes.
|
||||
* (1102-1100)*588 = 2*588 = 1176. Correct.
|
||||
* 닭 (U+B2ED) = AC00 + (1103-1100)*588 + (1161-1161)*28 + (11AF-11A7) -- wait닭 has T=U+11BC?
|
||||
* 닭: L=U+1103 (TIKEUT), V=U+1161 (A), T=U+11BC (IEUNG)? No.
|
||||
* Let's just use simple known values:
|
||||
* 가 U+AC00: L=U+1100, V=U+1161 → AC00 + 0 + 0 = AC00 ✓
|
||||
* 각 U+AC01: L=U+1100, V=U+1161, T=U+11A8 → AC00 + 0 + (11A8-11A7)=1 = AC01 ✓
|
||||
* 갈 U+AC08: L=U+1100, V=U+1161, T=U+11AF → AC00 + 7 = AC07? 11AF-11A7=8, so AC00+8=AC08 ✓
|
||||
* 나 U+B098: L=U+1102, V=U+1161 → AC00+2*588=AC00+0x498=B098 -- 0xAC00+0x498=0xB098?
|
||||
* 0xAC00=44032, 2*588=1176, 44032+1176=45208=0xB098. ✓
|
||||
*/
|
||||
public static function hangulDataProvider(): array
|
||||
{
|
||||
return [
|
||||
// Empty input returns empty output
|
||||
'empty' => [
|
||||
[],
|
||||
[],
|
||||
],
|
||||
|
||||
// Rule 2 with a precomposed LV syllable already in the input:
|
||||
// U+AC00 (GA) + U+11A8 (KIYEOK) -> U+AC01 (GAG)
|
||||
'precomposed_lv_plus_t' => [
|
||||
[0xAC00, 0x11A8],
|
||||
[0xAC01],
|
||||
],
|
||||
|
||||
// A syllable that already has a trailing consonant is not composed again
|
||||
'lvt_plus_t' => [
|
||||
[0xAC01, 0x11A8],
|
||||
[0xAC01, 0x11A8],
|
||||
],
|
||||
|
||||
// A precomposed LV syllable followed by anything else is left alone
|
||||
'precomposed_lv_plus_ascii' => [
|
||||
[0xAC00, 0x41],
|
||||
[0xAC00, 0x41],
|
||||
],
|
||||
|
||||
// Pure ASCII: no Hangul, pass through unchanged
|
||||
'ascii_only' => [
|
||||
[0x41, 0x42, 0x43],
|
||||
[0x41, 0x42, 0x43],
|
||||
],
|
||||
|
||||
// Leading consonant at end of array (no following vowel): unchanged
|
||||
// U+1100 KIYEOK alone
|
||||
'lone_leading_consonant' => [
|
||||
[0x1100],
|
||||
[0x1100],
|
||||
],
|
||||
|
||||
// First leading consonant, last leading consonant: boundary check
|
||||
// U+1100, U+1112: no vowels follow; both pass through
|
||||
'leading_consonant_boundaries' => [
|
||||
[0x1100, 0x1112],
|
||||
[0x1100, 0x1112],
|
||||
],
|
||||
|
||||
// Codepoint just above leading consonant range (U+1113): not L, pass through
|
||||
'above_leading_consonant_range' => [
|
||||
[0x1113],
|
||||
[0x1113],
|
||||
],
|
||||
|
||||
// Vowel alone: not a leading consonant, pass through
|
||||
// U+1161 JUNGSEONG A
|
||||
'lone_vowel' => [
|
||||
[0x1161],
|
||||
[0x1161],
|
||||
],
|
||||
|
||||
// Trailing consonant alone: not a leading consonant, pass through
|
||||
// U+11A8 JONGSEONG KIYEOK
|
||||
'lone_trailing_consonant' => [
|
||||
[0x11A8],
|
||||
[0x11A8],
|
||||
],
|
||||
|
||||
// L + V → LV syllable (no trailing consonant)
|
||||
// U+1100 + U+1161 → U+AC00 가 (GA)
|
||||
'l_plus_v_ga' => [
|
||||
[0x1100, 0x1161],
|
||||
[0xAC00],
|
||||
],
|
||||
|
||||
// L + V boundary: last L (U+1112) + last V (U+1175) → syllable
|
||||
// S = AC00 + 18*588 + 20*28 = AC00 + 10584 + 560 = AC00 + 11144 = D7A4 - 28 = D784?
|
||||
// 0xAC00 + 18*588 + 20*28 = 44032 + 10584 + 560 = 55176 = 0xD788
|
||||
'l_plus_v_boundary' => [
|
||||
[0x1112, 0x1175],
|
||||
[0xD788],
|
||||
],
|
||||
|
||||
// L + V + T → LVT syllable
|
||||
// U+1100 + U+1161 + U+11A8 → U+AC01 각 (GAK)
|
||||
// LV = AC00, T = 11A8 − 11A7 = 1 → AC00 + 1 = AC01
|
||||
'l_plus_v_plus_t_gak' => [
|
||||
[0x1100, 0x1161, 0x11A8],
|
||||
[0xAC01],
|
||||
],
|
||||
|
||||
// L + V + T with T = last valid trailing consonant (U+11C2)
|
||||
// U+1100 + U+1161 + U+11C2 → AC00 + (11C2 − 11A7) = AC00 + 27 = AC1B
|
||||
'l_plus_v_plus_t_last_trailing' => [
|
||||
[0x1100, 0x1161, 0x11C2],
|
||||
[0xAC1B],
|
||||
],
|
||||
|
||||
// L + V + TBase (U+11A7): TBase itself is NOT a valid trailing
|
||||
// consonant; treated as next non-T codepoint. LV emitted, then
|
||||
// U+11A7 passed through unchanged.
|
||||
'l_plus_v_plus_tbase_not_trailing' => [
|
||||
[0x1100, 0x1161, 0x11A7],
|
||||
[0xAC00, 0x11A7],
|
||||
],
|
||||
|
||||
// L + V + codepoint above T range (U+11C3): not a trailing consonant,
|
||||
// LV emitted then U+11C3 passed through
|
||||
'l_plus_v_then_above_t_range' => [
|
||||
[0x1100, 0x1161, 0x11C3],
|
||||
[0xAC00, 0x11C3],
|
||||
],
|
||||
|
||||
// L followed by non-vowel (ASCII): L emitted unchanged, then ASCII
|
||||
'leading_consonant_then_ascii' => [
|
||||
[0x1100, 0x41],
|
||||
[0x1100, 0x41],
|
||||
],
|
||||
|
||||
// Two separate LV syllables in sequence
|
||||
// U+1100+U+1161, U+1102+U+1161 → U+AC00, U+B098
|
||||
// B098: AC00 + 2*588 = AC00 + 1176 = 0xB098
|
||||
'two_lv_syllables' => [
|
||||
[0x1100, 0x1161, 0x1102, 0x1161],
|
||||
[0xAC00, 0xB098],
|
||||
],
|
||||
|
||||
// Mixed: ASCII + Jamo cluster + ASCII
|
||||
// 0x41, U+1100, U+1161, 0x42 → 0x41, U+AC00, 0x42
|
||||
'mixed_ascii_hangul' => [
|
||||
[0x41, 0x1100, 0x1161, 0x42],
|
||||
[0x41, 0xAC00, 0x42],
|
||||
],
|
||||
|
||||
// L + V + T + next L + V: two clusters in series
|
||||
// U+1100, U+1161, U+11A8, U+1102, U+1161 → U+AC01, U+B098
|
||||
'two_clusters_with_trailing' => [
|
||||
[0x1100, 0x1161, 0x11A8, 0x1102, 0x1161],
|
||||
[0xAC01, 0xB098],
|
||||
],
|
||||
|
||||
// L + first vowel out-of-range: U+1160 is just below VBASE: not a vowel
|
||||
'leading_consonant_then_below_vbase' => [
|
||||
[0x1100, 0x1160],
|
||||
[0x1100, 0x1160],
|
||||
],
|
||||
|
||||
// L + first codepoint above vowel range: U+1176: not a vowel
|
||||
'leading_consonant_then_above_vrange' => [
|
||||
[0x1100, 0x1176],
|
||||
[0x1100, 0x1176],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testNormalizesSparseIndexes(): void
|
||||
{
|
||||
$obj = new Hangul([10 => 0x1100, 20 => 0x1161]);
|
||||
$this->assertSame([0xAC00], $obj->getOrdarr());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ThaiTest.php
|
||||
*
|
||||
* @since 2026-04-30
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*
|
||||
* This file is part of tc-lib-unicode software library.
|
||||
*/
|
||||
|
||||
namespace Test\Substitution;
|
||||
|
||||
use Com\Tecnick\Unicode\Substitution\Thai;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Test\TestUtil;
|
||||
|
||||
/**
|
||||
* Thai substitution test
|
||||
*
|
||||
* @since 2026-04-30
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*/
|
||||
class ThaiTest extends TestUtil
|
||||
{
|
||||
/**
|
||||
* @param array<int, int> $input
|
||||
* @param array<int, int> $expected
|
||||
*/
|
||||
#[DataProvider('thaiDataProvider')]
|
||||
public function testGetOrdarr(array $input, array $expected): void
|
||||
{
|
||||
$obj = new Thai($input);
|
||||
$this->assertSame($expected, $obj->getOrdarr());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{0: array<int, int>, 1: array<int, int>}>
|
||||
*/
|
||||
public static function thaiDataProvider(): array
|
||||
{
|
||||
return [
|
||||
// Empty input returns empty output
|
||||
'empty' => [
|
||||
[],
|
||||
[],
|
||||
],
|
||||
|
||||
// Pure ASCII: no Thai codepoints, pass through unchanged
|
||||
'ascii_only' => [
|
||||
[0x41, 0x42, 0x43],
|
||||
[0x41, 0x42, 0x43],
|
||||
],
|
||||
|
||||
// Thai consonant only (no leading vowel): unchanged
|
||||
// U+0E01 THAI CHARACTER KO KAI
|
||||
'consonant_only' => [
|
||||
[0x0E01],
|
||||
[0x0E01],
|
||||
],
|
||||
|
||||
// Tone mark only: not a leading vowel, unchanged
|
||||
// U+0E48 THAI CHARACTER MAI EK
|
||||
'tone_mark_only' => [
|
||||
[0x0E48],
|
||||
[0x0E48],
|
||||
],
|
||||
|
||||
// Single leading vowel at end of array (orphaned): leave unchanged
|
||||
// U+0E40 THAI CHARACTER SARA E
|
||||
'orphaned_leading_vowel_end' => [
|
||||
[0x0E40],
|
||||
[0x0E40],
|
||||
],
|
||||
|
||||
// Leading vowel followed by a tone mark (not a base consonant):
|
||||
// leave unchanged: U+0E40, U+0E48
|
||||
'leading_vowel_then_tone_mark' => [
|
||||
[0x0E40, 0x0E48],
|
||||
[0x0E40, 0x0E48],
|
||||
],
|
||||
|
||||
// Leading vowel followed by an ASCII character (not a base
|
||||
// consonant): leave unchanged: U+0E40, 0x41
|
||||
'leading_vowel_then_ascii' => [
|
||||
[0x0E40, 0x41],
|
||||
[0x0E40, 0x41],
|
||||
],
|
||||
|
||||
// Preposed vowel: stored and displayed before the consonant
|
||||
// U+0E40 (SARA E) + U+0E01 (KO KAI)
|
||||
'sara_e_before_ko_kai' => [
|
||||
[0x0E40, 0x0E01],
|
||||
[0x0E40, 0x0E01],
|
||||
],
|
||||
|
||||
// U+0E41 (SARA AE) + U+0E02 (KHO KHAI)
|
||||
'sara_ae_before_kho_khai' => [
|
||||
[0x0E41, 0x0E02],
|
||||
[0x0E41, 0x0E02],
|
||||
],
|
||||
|
||||
// U+0E44 (SARA AI MAIMALAI) + last base consonant U+0E2E
|
||||
'sara_ai_before_ho_nokhuk' => [
|
||||
[0x0E44, 0x0E2E],
|
||||
[0x0E44, 0x0E2E],
|
||||
],
|
||||
|
||||
// Multiple consecutive preposed vowels before one consonant
|
||||
'two_leading_vowels_then_consonant' => [
|
||||
[0x0E40, 0x0E41, 0x0E01],
|
||||
[0x0E40, 0x0E41, 0x0E01],
|
||||
],
|
||||
|
||||
// Preposed vowel + consonant + tone mark
|
||||
'vowel_consonant_tone' => [
|
||||
[0x0E40, 0x0E01, 0x0E48],
|
||||
[0x0E40, 0x0E01, 0x0E48],
|
||||
],
|
||||
|
||||
// Mixed: ASCII + Thai cluster + ASCII
|
||||
'mixed_ascii_thai' => [
|
||||
[0x41, 0x0E40, 0x0E01, 0x0E48, 0x42],
|
||||
[0x41, 0x0E40, 0x0E01, 0x0E48, 0x42],
|
||||
],
|
||||
|
||||
// Two separate Thai clusters in one array
|
||||
'two_clusters' => [
|
||||
[0x0E40, 0x0E01, 0x0E44, 0x0E2E],
|
||||
[0x0E40, 0x0E01, 0x0E44, 0x0E2E],
|
||||
],
|
||||
|
||||
// Multiple consecutive leading vowels followed by non-consonant
|
||||
// (both left unchanged)
|
||||
'two_leading_vowels_no_consonant' => [
|
||||
[0x0E40, 0x0E41],
|
||||
[0x0E40, 0x0E41],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testNormalizesSparseIndexes(): void
|
||||
{
|
||||
$obj = new Thai([3 => 0x0E40, 7 => 0x0E01]);
|
||||
$this->assertSame([0x0E40, 0x0E01], $obj->getOrdarr());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SubstitutionTest.php
|
||||
*
|
||||
* @since 2026-04-30
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*
|
||||
* This file is part of tc-lib-unicode software library.
|
||||
*/
|
||||
|
||||
namespace Test;
|
||||
|
||||
use Com\Tecnick\Unicode\Substitution;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
/**
|
||||
* Substitution dispatcher test
|
||||
*
|
||||
* @since 2026-04-30
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*/
|
||||
class SubstitutionTest extends TestUtil
|
||||
{
|
||||
protected function getTestObject(): Substitution
|
||||
{
|
||||
return new Substitution();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $input
|
||||
* @param array<int, int> $expected
|
||||
*/
|
||||
#[DataProvider('replaceCharsDataProvider')]
|
||||
public function testReplaceChars(array $input, array $expected): void
|
||||
{
|
||||
$sub = $this->getTestObject();
|
||||
$this->assertSame($expected, $sub->replaceChars($input));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{0: array<int, int>, 1: array<int, int>}>
|
||||
*/
|
||||
public static function replaceCharsDataProvider(): array
|
||||
{
|
||||
return [
|
||||
// Empty input
|
||||
'empty' => [
|
||||
[],
|
||||
[],
|
||||
],
|
||||
|
||||
// Pure ASCII: Thai script not detected, returned unchanged
|
||||
'ascii_only' => [
|
||||
[0x41, 0x42, 0x43],
|
||||
[0x41, 0x42, 0x43],
|
||||
],
|
||||
|
||||
// Non-Thai non-Devanagari Unicode (Bengali): not yet handled, pass through
|
||||
// U+0985 BENGALI LETTER A
|
||||
'bengali_passthrough' => [
|
||||
[0x0985, 0x0986],
|
||||
[0x0985, 0x0986],
|
||||
],
|
||||
|
||||
// Thai only consonants (Thai detected but no leading vowels):
|
||||
// handler runs but nothing is repositioned
|
||||
// U+0E01 KO KAI, U+0E02 KHO KHAI
|
||||
'thai_consonants_only' => [
|
||||
[0x0E01, 0x0E02],
|
||||
[0x0E01, 0x0E02],
|
||||
],
|
||||
|
||||
// Thai with multiple codepoints: exercises the detectScripts
|
||||
// short-circuit where the second Thai codepoint skips the check
|
||||
// U+0E01, U+0E02, U+0E03
|
||||
'thai_multiple_consonants' => [
|
||||
[0x0E01, 0x0E02, 0x0E03],
|
||||
[0x0E01, 0x0E02, 0x0E03],
|
||||
],
|
||||
|
||||
// Thai with a preposed vowel: the dispatcher delegates to ThaiHandler,
|
||||
// which keeps the stored order
|
||||
'thai_preposed_vowel' => [
|
||||
[0x0E40, 0x0E01],
|
||||
[0x0E40, 0x0E01],
|
||||
],
|
||||
|
||||
// Mixed script: Thai cluster plus ASCII
|
||||
'mixed_ascii_thai' => [
|
||||
[0x41, 0x0E40, 0x0E01, 0x42],
|
||||
[0x41, 0x0E40, 0x0E01, 0x42],
|
||||
],
|
||||
|
||||
// Devanagari consonant without matra: handler runs, nothing moved
|
||||
// U+0915 KA, U+0916 KHA
|
||||
'devanagari_consonants_only' => [
|
||||
[0x0915, 0x0916],
|
||||
[0x0915, 0x0916],
|
||||
],
|
||||
|
||||
// Devanagari: detectScripts short-circuit: second Devanagari
|
||||
// codepoint skips the range check once already detected
|
||||
// U+0915, U+0916, U+0917
|
||||
'devanagari_multiple_consonants' => [
|
||||
[0x0915, 0x0916, 0x0917],
|
||||
[0x0915, 0x0916, 0x0917],
|
||||
],
|
||||
|
||||
// Devanagari left matra reposition via dispatcher
|
||||
// U+0915 KA + U+093F → U+093F, U+0915
|
||||
'devanagari_matra_reposition' => [
|
||||
[0x0915, 0x093F],
|
||||
[0x093F, 0x0915],
|
||||
],
|
||||
|
||||
// Mixed ASCII + Devanagari cluster
|
||||
// 0x41, U+0915, U+093F, 0x42 → 0x41, U+093F, U+0915, 0x42
|
||||
'mixed_ascii_devanagari' => [
|
||||
[0x41, 0x0915, 0x093F, 0x42],
|
||||
[0x41, 0x093F, 0x0915, 0x42],
|
||||
],
|
||||
|
||||
// Devanagari codepoint in block but outside consonant range:
|
||||
// U+0900 INVERTED CANDRABINDU (combining mark): no matra reorder
|
||||
'devanagari_non_consonant' => [
|
||||
[0x0900, 0x093F],
|
||||
[0x0900, 0x093F],
|
||||
],
|
||||
|
||||
// Hangul Jamo leading consonant + vowel: dispatcher delegates
|
||||
// U+1100 + U+1161 → U+AC00 가 (GA)
|
||||
'hangul_lv_composition' => [
|
||||
[0x1100, 0x1161],
|
||||
[0xAC00],
|
||||
],
|
||||
|
||||
// Hangul Jamo detected via extended-A range (U+A960): triggers hangul handler;
|
||||
// U+A960 is not in the standard L range so it passes through unchanged but
|
||||
// ensures isHangulJamo covers HANGUL_JAMO_EXT_A
|
||||
'hangul_ext_a_passthrough' => [
|
||||
[0xA960],
|
||||
[0xA960],
|
||||
],
|
||||
|
||||
// Hangul Jamo detected via extended-B range (U+D7B0): same as above for EXT_B
|
||||
'hangul_ext_b_passthrough' => [
|
||||
[0xD7B0],
|
||||
[0xD7B0],
|
||||
],
|
||||
|
||||
// Hangul: detectScripts short-circuit: second Hangul Jamo codepoint skips check
|
||||
// U+1100, U+1102: both L, no V, so no composition
|
||||
'hangul_multiple_leading_consonants' => [
|
||||
[0x1100, 0x1102],
|
||||
[0x1100, 0x1102],
|
||||
],
|
||||
|
||||
// Hangul L + V + T full composition via dispatcher
|
||||
// U+1100, U+1161, U+11A8 → U+AC01
|
||||
'hangul_lvt_composition' => [
|
||||
[0x1100, 0x1161, 0x11A8],
|
||||
[0xAC01],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* TestUtil.php
|
||||
*
|
||||
* @since 2020-12-19
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2015-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*
|
||||
* This file is part of tc-lib-color software library.
|
||||
*/
|
||||
|
||||
namespace Test;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Web Color class test
|
||||
*
|
||||
* @since 2020-12-19
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2015-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*/
|
||||
class TestUtil extends TestCase
|
||||
{
|
||||
/**
|
||||
* @param class-string<\Throwable> $exception
|
||||
*/
|
||||
public function bcExpectException(string $exception): void
|
||||
{
|
||||
parent::expectException($exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* TextDirectionTest.php
|
||||
*
|
||||
* @since 2026-07-17
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*
|
||||
* This file is part of tc-lib-unicode software library.
|
||||
*/
|
||||
|
||||
namespace Test;
|
||||
|
||||
use Com\Tecnick\Unicode\Bidi;
|
||||
use Com\Tecnick\Unicode\TextDirection;
|
||||
|
||||
/**
|
||||
* TextDirection enum test
|
||||
*
|
||||
* @since 2026-07-17
|
||||
* @category Library
|
||||
* @package Unicode
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-unicode
|
||||
*/
|
||||
class TextDirectionTest extends TestUtil
|
||||
{
|
||||
public function testCaseBackingValues(): void
|
||||
{
|
||||
$this->assertSame('', TextDirection::Auto->value);
|
||||
$this->assertSame('R', TextDirection::Rtl->value);
|
||||
$this->assertSame('L', TextDirection::Ltr->value);
|
||||
}
|
||||
|
||||
public function testFromLooseCanonical(): void
|
||||
{
|
||||
$this->assertSame(TextDirection::Auto, TextDirection::fromLoose(''));
|
||||
$this->assertSame(TextDirection::Rtl, TextDirection::fromLoose('R'));
|
||||
$this->assertSame(TextDirection::Ltr, TextDirection::fromLoose('L'));
|
||||
}
|
||||
|
||||
public function testFromLooseIsLenientOnFirstCharacter(): void
|
||||
{
|
||||
$this->assertSame(TextDirection::Rtl, TextDirection::fromLoose('r'));
|
||||
$this->assertSame(TextDirection::Ltr, TextDirection::fromLoose('l'));
|
||||
$this->assertSame(TextDirection::Rtl, TextDirection::fromLoose('RTL'));
|
||||
$this->assertSame(TextDirection::Ltr, TextDirection::fromLoose('ltr'));
|
||||
$this->assertSame(TextDirection::Rtl, TextDirection::fromLoose('right'));
|
||||
$this->assertSame(TextDirection::Ltr, TextDirection::fromLoose('left'));
|
||||
}
|
||||
|
||||
public function testFromLooseFallsBackToAuto(): void
|
||||
{
|
||||
$this->assertSame(TextDirection::Auto, TextDirection::fromLoose('X'));
|
||||
$this->assertSame(TextDirection::Auto, TextDirection::fromLoose('auto'));
|
||||
$this->assertSame(TextDirection::Auto, TextDirection::fromLoose('1'));
|
||||
}
|
||||
|
||||
public function testFromLoosePassesThroughEnumInstance(): void
|
||||
{
|
||||
$this->assertSame(TextDirection::Rtl, TextDirection::fromLoose(TextDirection::Rtl));
|
||||
}
|
||||
|
||||
public function testFromLooseRoundTrip(): void
|
||||
{
|
||||
foreach (TextDirection::cases() as $case) {
|
||||
$this->assertSame($case, TextDirection::fromLoose($case->value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The widened Bidi constructor accepts a TextDirection and behaves exactly
|
||||
* like the equivalent legacy string.
|
||||
*
|
||||
* @throws \Com\Tecnick\Unicode\Exception
|
||||
*/
|
||||
public function testBidiAcceptsEnum(): void
|
||||
{
|
||||
// The trailing space makes the forced RTL direction visible in the output:
|
||||
// L1.4 resets it to the paragraph level, moving it to the visual left.
|
||||
$fromEnum = new Bidi('left to right ', null, null, TextDirection::Rtl, true);
|
||||
$fromString = new Bidi('left to right ', null, null, 'R', true);
|
||||
$this->assertSame(' left to right', $fromEnum->getString());
|
||||
$this->assertSame($fromString->getString(), $fromEnum->getString());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user