generated from jric11/baseProject
231 lines
8.3 KiB
PHP
231 lines
8.3 KiB
PHP
|
|
<?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());
|
|||
|
|
}
|
|||
|
|
}
|