Skip to content

Commit d165e61

Browse files
committed
feat: add enum with enum-with-union-type pattern
1 parent 2489b2d commit d165e61

6 files changed

Lines changed: 239 additions & 4 deletions

File tree

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.6.4
1+
2.7.0

src/BidiClass.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* BidiClass.php
7+
*
8+
* @since 2026-07-17
9+
* @category Library
10+
* @package UnicodeData
11+
* @author Nicola Asuni <info@tecnick.com>
12+
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
13+
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
14+
* @link https://github.com/tecnickcom/tc-lib-unicode-data
15+
*
16+
* This file is part of tc-lib-unicode-data software library.
17+
*/
18+
19+
namespace Com\Tecnick\Unicode\Data;
20+
21+
/**
22+
* Com\Tecnick\Unicode\Data\BidiClass
23+
*
24+
* Backed enum for the strong, weak and neutral bidirectional character classes.
25+
* The backing value of each case is the canonical class abbreviation used as the
26+
* keys of Type::STRONG, Type::WEAK and Type::NEUTRAL. The explicit formatting
27+
* codes (LRE, LRO, RLE, RLO, PDF, ...) are intentionally not part of this set.
28+
*
29+
* @since 2026-07-17
30+
* @category Library
31+
* @package UnicodeData
32+
* @author Nicola Asuni <info@tecnick.com>
33+
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
34+
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
35+
* @link https://github.com/tecnickcom/tc-lib-unicode-data
36+
*/
37+
enum BidiClass: string
38+
{
39+
// Strong types (Type::STRONG).
40+
case L = 'L';
41+
42+
case R = 'R';
43+
44+
case AL = 'AL';
45+
46+
// Weak types (Type::WEAK).
47+
case EN = 'EN';
48+
49+
case ES = 'ES';
50+
51+
case ET = 'ET';
52+
53+
case AN = 'AN';
54+
55+
case CS = 'CS';
56+
57+
case NSM = 'NSM';
58+
59+
case BN = 'BN';
60+
61+
// Neutral types (Type::NEUTRAL).
62+
case B = 'B';
63+
64+
case S = 'S';
65+
66+
case WS = 'WS';
67+
68+
case ON = 'ON';
69+
70+
/**
71+
* Resolve a loose bidirectional class value to the matching enum case.
72+
*
73+
* Accepts the canonical class abbreviation or an enum instance (returned
74+
* unchanged). Unknown values throw a \ValueError, matching the closed set
75+
* defined by Type::STRONG, Type::WEAK and Type::NEUTRAL.
76+
*
77+
* @param string|self $value Bidirectional class abbreviation or enum case.
78+
*/
79+
public static function fromLoose(string|self $value): self
80+
{
81+
if ($value instanceof self) {
82+
return $value;
83+
}
84+
85+
return self::from($value);
86+
}
87+
88+
/**
89+
* True for a strong type (L, R, AL).
90+
*/
91+
public function isStrong(): bool
92+
{
93+
return \array_key_exists($this->value, Type::STRONG);
94+
}
95+
96+
/**
97+
* True for a weak type (EN, ES, ET, AN, CS, NSM, BN).
98+
*/
99+
public function isWeak(): bool
100+
{
101+
return \array_key_exists($this->value, Type::WEAK);
102+
}
103+
104+
/**
105+
* True for a neutral type (B, S, WS, ON).
106+
*/
107+
public function isNeutral(): bool
108+
{
109+
return \array_key_exists($this->value, Type::NEUTRAL);
110+
}
111+
}

src/Constant.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,9 @@ class Constant
142142
* They act exactly like right-to-left or left-to-right characters,
143143
* except that they do not display or have any other semantic effect.
144144
* Their use is more convenient than using explicit embeddings or overrides because their scope is much more local.
145-
*
146-
* /**
145+
*/
146+
147+
/**
147148
* (U+200E) LEFT-TO-RIGHT MARK
148149
* Left-to-right zero-width character
149150
*/

src/Pattern.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
class Pattern
3333
{
3434
/**
35-
* Pattern to test RTL (Righ-To-Left) strings using regular expressions.
35+
* Pattern to test RTL (Right-To-Left) strings using regular expressions.
3636
* (excluding Arabic)
3737
*/
3838
public const RTL = "/(

src/Type.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17815,4 +17815,19 @@ class Type
1781517815
1_048_576 => 'L',
1781617816
1_114_109 => 'L',
1781717817
];
17818+
17819+
/**
17820+
* Get the simple (non-explicit) bidirectional class of a Unicode code point
17821+
* as a typed enum case.
17822+
*
17823+
* Returns null when the code point is unmapped or maps to an explicit
17824+
* formatting code (LRE, LRO, RLE, RLO, PDF), which are not part of the
17825+
* strong/weak/neutral BidiClass set.
17826+
*
17827+
* @param int $ord Unicode code point.
17828+
*/
17829+
public static function getBidiClass(int $ord): ?BidiClass
17830+
{
17831+
return BidiClass::tryFrom(self::UNI[$ord] ?? '');
17832+
}
1781817833
}

test/BidiClassTest.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
/**
4+
* BidiClassTest.php
5+
*
6+
* @since 2026-07-17
7+
* @category Library
8+
* @package UnicodeData
9+
* @author Nicola Asuni <info@tecnick.com>
10+
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
11+
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
12+
* @link https://github.com/tecnickcom/tc-lib-unicode-data
13+
*
14+
* This file is part of tc-lib-unicode-data software library.
15+
*/
16+
17+
namespace Test;
18+
19+
use Com\Tecnick\Unicode\Data\BidiClass;
20+
use Com\Tecnick\Unicode\Data\Type;
21+
use PHPUnit\Framework\TestCase;
22+
23+
/**
24+
* BidiClass enum test
25+
*
26+
* @since 2026-07-17
27+
* @category Library
28+
* @package UnicodeData
29+
* @author Nicola Asuni <info@tecnick.com>
30+
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
31+
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
32+
* @link https://github.com/tecnickcom/tc-lib-unicode-data
33+
*/
34+
class BidiClassTest extends TestCase
35+
{
36+
public function testCasesMatchTypeGroups(): void
37+
{
38+
$values = \array_map(static fn(BidiClass $case): string => $case->value, BidiClass::cases());
39+
$expected = \array_merge(\array_keys(Type::STRONG), \array_keys(Type::WEAK), \array_keys(Type::NEUTRAL));
40+
$this->assertSame($expected, $values);
41+
}
42+
43+
public function testFromLooseCanonical(): void
44+
{
45+
$this->assertSame(BidiClass::L, BidiClass::fromLoose('L'));
46+
$this->assertSame(BidiClass::NSM, BidiClass::fromLoose('NSM'));
47+
$this->assertSame(BidiClass::ON, BidiClass::fromLoose('ON'));
48+
}
49+
50+
public function testFromLoosePassesThroughEnumInstance(): void
51+
{
52+
$this->assertSame(BidiClass::AL, BidiClass::fromLoose(BidiClass::AL));
53+
}
54+
55+
public function testFromLooseRoundTrip(): void
56+
{
57+
foreach (BidiClass::cases() as $case) {
58+
$this->assertSame($case, BidiClass::fromLoose($case->value));
59+
}
60+
}
61+
62+
public function testFromLooseUnknownThrows(): void
63+
{
64+
$this->expectException(\ValueError::class);
65+
BidiClass::fromLoose('ZZ');
66+
}
67+
68+
public function testFromLooseRejectsExplicitFormattingCode(): void
69+
{
70+
// LRE is a valid UNI value but an explicit formatting code, not a BidiClass.
71+
$this->expectException(\ValueError::class);
72+
BidiClass::fromLoose('LRE');
73+
}
74+
75+
public function testCategoryHelpers(): void
76+
{
77+
$this->assertTrue(BidiClass::R->isStrong());
78+
$this->assertFalse(BidiClass::R->isWeak());
79+
$this->assertFalse(BidiClass::R->isNeutral());
80+
81+
$this->assertTrue(BidiClass::EN->isWeak());
82+
$this->assertFalse(BidiClass::EN->isStrong());
83+
84+
$this->assertTrue(BidiClass::WS->isNeutral());
85+
$this->assertFalse(BidiClass::WS->isStrong());
86+
}
87+
88+
public function testEveryCaseHasExactlyOneCategory(): void
89+
{
90+
foreach (BidiClass::cases() as $case) {
91+
$count = (int) $case->isStrong() + (int) $case->isWeak() + (int) $case->isNeutral();
92+
$this->assertSame(1, $count, 'BidiClass ' . $case->value . ' must belong to exactly one category');
93+
}
94+
}
95+
96+
public function testGetBidiClassForMappedCodePoint(): void
97+
{
98+
$this->assertSame(BidiClass::L, Type::getBidiClass(65)); // 'A'
99+
$this->assertSame(BidiClass::WS, Type::getBidiClass(32)); // space
100+
$this->assertSame(BidiClass::B, Type::getBidiClass(10)); // line feed
101+
}
102+
103+
public function testGetBidiClassIsNullForExplicitOrUnmapped(): void
104+
{
105+
$this->assertNull(Type::getBidiClass(8234)); // LRE explicit formatting code
106+
$this->assertNull(Type::getBidiClass(0x10FFFF)); // unmapped code point
107+
}
108+
}

0 commit comments

Comments
 (0)