۱۳ ژوئیه ۲۰۲۳

یونی‌کد، درون رشته‌ها

اطلاعات بیشتر

این بخش به دنبال عمیق شدن در رشته‌ها است. این دانش برای شما مفید خواهد بود اگر قصد دارید با ایموجی‌ها، کاراکترهای ریاضی نادر یا هیروگلیف‌ها یا سایر نمادهای نادر سر و کار داشته باشید.

همانطور که می‌دانیم رشته‌ها (strings) در جاوااسکریپت بر پایه یونی‌کد هستند و هر کاراکتر نماینده دنباله‌ای 1 تا 4 عضوی از بایت‌ها است.

جاوااسکریپت اجازه درج کاراکتر توسط یونی‌کد هگزادسیمال آن درون یک رشته را با یکی از این سه نماد می‌دهد:

  • ‎\xXX

    به جای XX باید دو عدد هگزادسیمال در بازه 00 و FF قرار گیرد. در نتیجه ‎\xXX کاراکتری است که یونی‌کد آن XX هست.

    از آنجا که نماد ‎\xXX فقط از دو عدد هگزادسیمال پشتیبانی می‌کند، از آن می‌توان فقط برای 256 کاراکتر اول یونی‌کد استفاده کرد.

    این 256 کاراکتر اول شامل الفبای لاتین ، کاراکتر های نحوی ابتدایی و برخی چیزهای دیگر می‌شود. به عنوان مثال "‎\x7A" مانند نوشتن "z" هست. (یونی‌کد U+007A)

    alert( "\x7A" ); // z
    alert( "\xA9" ); // ©, نماد کپی‌رایت
  • ‎\uXXXX

    به جای XXXX باید 4 عدد هگزادسیمال در بازه 0000 و FFFF قرار گیرد. در نتیجه ‎\uXXXX کاراکتری است که یونی‌کد آن XXXX هست. کاراکترهایی بامقادیر یونی‌کد بزرگتر از U+FFFF را هم می‌توان با این روش نشان داد. برای آن باید از یک جفت جایگزین (surrogate pair) استفاده کرد. (در ادامه بیشتر در رابطه با آنها صحبت خواهیم کرد.)

    alert( "\u00A9" ); // ©, با استفاده از نماد هگز 4 رقمی \xA9 مانند
    alert( "\u044F" ); // я, حرف 'یَه' در الفبای سیریلیک
    alert( "\u2191" ); // ↑, نماد فلش رو به بالا
  • ‎\u{X…XXXXXX}

    به جای X…XXXXXX باید یک مقدار هگزادسیمال 1 تا 6 بایت در بازه 0 و 10FFFF قرار گیرد (بالاترین مقدار تعریف شده توسط یونی‌کد). توسط این نماد می‌توان تمامی یونی‌کد های موجود را به راحتی نشان داد.

    alert( "\u{20331}" ); // 佫, یک کاراکتر چینی نادر (دارای یونی‌کد طولانی)
    alert( "\u{1F60D}" ); // 😍, ایموجی چهره خندان (یونی‌کد طولانی دیگر)

جفت جایگزین (Surrogate pairs)

تمامی کاراکترهای متداول دارای کدهای 2 بایتی هستند (4 رقم هگز). حروف در بیشتر زبان های اروپایی ، اعداد و مجموعه های ایدئوگرافیک چینی، ژاپنی و کره‌ای (CJK) ، با 2 بایت نمایش داده می‌شوند.

در ابتدا جاوا اسکریپت بر پایه رمزگذاری UTF-16 فقط 2 بایت را برای هر کاراکتر در نظر می‌گرفت. اما 2 بایت فقط برای نمایش 65536 ترکیب هست و این مقدار برای حالت های ممکن نمادهای یونی‌کد کافی نیست.

بنابراین نمادهای کمیاب که نیازمند بیش از دو بایت هستند. با یک جفت کاراکتر 2 بیتی به‌نام “جفت جایگزین (Surrogate pairs)” کدگذاری می‌شوند.

که تاثیر جانبی این اقدام افزایش طول کاراکتر به 2 است:

alert( '𝒳'.length ); // 2, در ریاضیات X نماد
alert( '😂'.length ); // 2, ایموجی لبخند
alert( '𩷶'.length ); // 2, یک کاراکتر چینی نادر

دلیل این اتفاق این است که در زمان ایجاد جاوااسکریپت جفت‌های جایگزین وجود نداشتند و بنابراین به درستی توسط زبان پردازش نمی‌شوند!

در حقیقت در هر یک از رشته‌های بالا یک نماد واحد داریم، در صورتی که ویژگی length مقدار 2 را نشان می‌دهد.

دریافت نماد نیز می‌تواند مشکل باشد، زیرا بیشتر اجزای زبان، جفت‌های جایگزین را به‌عنوان دو کاراکتر در نظر می‌گیرند.

به عنوان مثال، در اینجا می‌توان دو بخش یک کاراکتر را مشاهده نمود:

alert( '𝒳'[0] ); // بخش اول از جفت جانشین - نمایش نماد ناشناخته
alert( '𝒳'[1] ); // بخش دوم از جفت جانشین - نمایش نماد ناشناخته

قسمت‌های یک جفت جایگزین بدون یکدیگر معنایی ندارند. بنابراین پیغام‌های بالا چیزهای بی معنی نشان می‌دهند.

از نظر فنی جفت‌های جایگزین (surrogate pairs) را از طریق کدهایشان می‌‌توان تشخیص داد. اگر کاراکتر دارای کد در بازه 0xd800..0xdbff باشد ، آنگاه قسمت اول از جفت جایگزین (surrogate pairs) هست. کاراکتر بعد (قسمت دوم) باید در بازه 0xdc00..0xdfff باشد. این بازه‌ها به‌طور انحصاری برای جفت‌های جایگزین توسط استاندارد آن رزرو شده.

متد‌های String.fromCodePoint و str.codePointAt به جاوااسکریپت اضافه‌شدند تا با جفت‌های جایگزین (surrogate pairs) بتوان بدون مشکل عمل کرد.

این متدها بسیار شبیه String.fromCharCode و str.charCodeAt هستند، اما در رفتار با جفت‌های جایگزین (surrogate pairs) به درستی عمل می‌کنند.

تفاوت را در اینجا مشاهده کنید:

// را می‌دهد 𝒳 به جفت‌ جایگزین آگاه نیست برای همین فقط کد قسمت اول charCodeAt

alert( '𝒳'.charCodeAt(0).toString(16) ); // d835

// به جفت‌ جایگزین آگاه هست codePointAt
alert( '𝒳'.codePointAt(0).toString(16) ); // 1d4b3, هر دو قسمت را میخواند

زمانی که بخواهیم خانه دوم (ایندکس 1) را بخوانیم (حرکت نسبتاً اشتباه در این قسمت) هر دو فقط قسمت دوم جفت را بر می‌گردانند.

alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3
alert( '𝒳'.codePointAt(1).toString(16) ); // dcb3
// بخش دوم از جفت - نمایش نماد ناشناخته

شما راه‌های بیشتری در رابطه با نحوه برخورد با جفت‌های جایگزین (surrogate pairs) در فصل حلقه‌پذیرها خوهید آموخت. احتمالا کتابخانه‌های خاصی برای این امر وجود دارند، اما هیچ کدام آنها آنقدر معروف نیستند که اینجا معرفی شوند.

تقسیم رشته ها در یک نقطه دلخواه خطرناک است

نمی‌توان یک رشته را در یک قمست دلخواه تقسیم کرد و همیشه انتظار یک رشته معتبر داشت. به عنوان مثال “str.slice(0,4)”. به نمونه توجه کنید:

alert( 'hi 😂'.slice(0, 4) ); //  hi �

در اینجا می‌توان کاراکتر بی معنی (نمیه اول جفت جایگزین ایموجی لبخند) را در خروجی مشاهده نمود.

درصورتی که قصد دارید به‌طور غیر قابل اعتماد با جفت‌های جایگزین (surrogate pairs) کار کنید باید از این موضوع آگاه باشید. احتمالا مشکل بزرگی نیست، اما حداقل باید بدا نید چه اتفاقی درحال روی دادن است.

علائم حرکت گذاری و عادی سازی

در بسیاری از زبان های نوشتاری، نمادهایی وجود دارند که از یک کاراکتر پایه با یک علامت در پایین یا بالای آن تشکیل شده‌اند.

به عنوان مثال حرف پایه می‌تواند a باشد و نمونه های مقابل را درست کرد: àáâäãåā. در زبان فارسی هم نمونه‌هایی و جود دارد مانند تشدید در ملّت و حروف صدادار.

اکثر کاراکتر های رایج ترکیبات خود را هم در جدول یونی‌کد دارند. ولی نه همه آنها، زیرا تعداد ترکیبات ممکن بسیار زیاد خواهد شد.

برای پشتیبانی از همه ترکیبات دلخواه، استاندارد یونی‌کد این امکان را می‌دهد تا از چند کاراکتر یونی‌کد استفاده کنیم: یک کاراکتر پایه و یک یا چند کاراکتر “مارک” که به کاراکتر اول افزوده می‌شود و آن را تزئین می‌ کند.

به عنوان نمونه در صورتی که S با کاراکتر ویژه “نقطه در بالا” (کد ‎\u0307) بیاید، را نمایش می‌دهد.

alert( 'S‎\u0307' ); // Ṡ

اگر به علامت بیشتری نیاز دارید، فقط کاراکتر علامت مورد نیاز را اضافه کنید.

به عنوان نمونه در صورتی که در مرحله قبل “نقطه در پایین” (کد ‎\u0323) را هم اضافه کنیم، ما یک “s با نقطه در پایین و بالا” خواهیم داشت: Ṩ .

به عنوان مثال:

alert( 'S\u0307\u0323' ); // Ṩ

این انعطاف پذیری خوب می‌تواند مشکل جالبی را پیش بیاورد: دو کاراکتر می توانند ظاهری یکسان داشته باشند در صورتی که با ترکیبات مختلف یونی‌کد ساخته شده باشند.

به عنوان مثال:

let s1 = 'S\u0307\u0323'; // Ṩ, نقطه در بالا + نقطه در پایین + S
let s2 = 'S\u0323\u0307'; // Ṩ, نقطه در پایین + نقطه در بالا + S

alert( `s1: ${s1}, s2: ${s2}` ); // s1: Ṩ, s2: Ṩ

alert( s1 == s2 ); // می‌دهد در صورتی که یکسان بنظر می‌آیند (!؟) flase پاسخ

برای حل این مشکل، یک الگوریتم “نرمال سازی یونی‌کد” وجود داردکه هر رشته را بصورت فرم یکتا آن نرمال می‌کند.

و توسط str.normalize()‎ پیاده سازی شده.

alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true

خنده‌دار است که normalize()‎ در واقع دنباله ای از 3 کاراکتر را باهم جمع می‌کند: ‎\u1e68 (حرف s با دو نقطه).

alert( "S\u0307\u0323".normalize().length ); // 1

alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true

در واقعیت همیشه اینطور نیست. دلیل آن این است که نماد به اندازه ای مهم بود که سازندگان یونی‌کد آن را در جدول اصلی قرار دهند و این کد را به آن اختصاص دهند.

درصورتی که می‌خواهید در مورد قوانین و انواع نرمال سازی اطلاعات بیشتری کسب کنید آنها در پیوست استاندارد یونی‌کد توضیح داده شده‌اند: Unicode Normalization Forms ، ولی برای بیشتر اهداف کاربردی تا همین اندازه کافی است.

نقشه آموزش

نظرات

قبل از نظر دادن این را بخوانید…
  • اگر پیشنهادی برای بهبود ترجمه دارید - لطفا یک ایشوی گیت‌هاب یا یک پول‌ریکوئست به جای کامنت‌گذاشتن باز کنید.
  • اگر چیزی را در مقاله متوجه نمی‌شوید – به دقت توضیح دهید.
  • برای قراردادن یک خط از کد، از تگ <code> استفاده کنید، برای چندین خط – کد را درون تگ <pre> قرار دهید، برای بیش از ده خط کد – از یک جعبهٔ شنی استفاده کنید. (plnkr، jsbin، codepen…)