Unicode 字元轉換為 UTF16

剛好最近都在看編碼的東西,順便整理一下

Unicode

Unicode 又稱萬國碼,是一種電腦編碼規則,目的是為了解決不同語言之間的文字互通,目前,幾乎所有電腦系統都支援基本拉丁字母,並各自支援不同的其他編碼方式。Unicode 為了和它們相互相容,其首 256 個字元保留給 ISO 8859-1(也就是現在大家所熟知的,以 ASCII 為基礎,再添加附加符號、拉丁字母)所定義的字元

Unicode 字元的平面對應

目前 Unicode 分為 17 個平面,每個平面包含 65536 個字元,每個平面的字元都有一個獨一無二的點位,這個點位就是該字元的 Unicode 點位,這個點位是一個十六進位的數字,可以用來表示該字元,目前只用了少數平面,主要的就是前四個平面,其他的平面則是用來存放一些不常用的字元,或是一些特殊的字元

平面 範圍 中文名 英文名 簡稱
0 U+0000 至 U+FFFF 基本多文種平面 Basic Multilingual Plane BMP
1 U+10000 至 U+1FFFF 多文種補充平面 Supplementary Multilingual Plane SMP
2 U+20000 至 U+2FFFF 表意文字補充平面 Supplementary Ideographic Plane SIP
3 U+30000 至 U+3FFFF 表意文字第三平面 Tertiary Ideographic Plane TIP

ref:Unicode 字元平面對映

Unicode 的編碼

在表示一個 Unicode 字元的時候,會使用 U+4AE0 這樣的格式來表示,其中 U+ 是固定的,後面的數字則是該字元的 Unicode 點位。

UTF8

UTF8 的英文是 8-bit Unicode Transformation Format,是一種可以變動長度的字元表示方式

  1. 128 個 ASCII 字元,只需一個位元組編碼(Unicode 範圍由 U+0000 至 U+007F)
  2. 其他西歐國家的會用到的一些字元,少部分則需要用到兩個位元組(Unicode 範圍由 U+0080 至 U+07FF)
  3. 其他大部分的常用字元,包含中文、日文、韓文等,則需要用到三個位元組(Unicode 範圍由 U+0800 至 U+FFFF)
  4. 其他極少使用的 Unicode 輔助平面的字元(也就是 BMP 之外的輔助平面)使用四至六位元組編碼(Unicode 範圍由 U+10000 至 U+1FFFFF 使用四位元組,Unicode 範圍由 U+200000 至 U+3FFFFFF 使用五位元組,Unicode 範圍由 U+4000000 至 U+7FFFFFFF 使用六位元組)。

ref: 維基百科 UTF-8

UTF8 與 UTF16 的比較

在輔助平面字元的處理,兩者都需要使用 4 bytes 表示一個字元;但在 BMP 平面的字元處理上,UTF-8 使用 1-3 bytes 表示一個字元,而 UTF-16 則是固定使用 2 bytes 表示一個字元,這也是為什麼 UTF16 在處理 BMP 平面的字元時,會比 UTF8 來的節省空間

Unicode 點位轉換的計算過程 (輔助平面的字元)

𠎀這個字的 Unicode 點位: 0x20380

首先需要將其扣除 0x10000,因為該點位超過了 0xFFFF,並不屬於 BMP 範圍,所以需要使用 4 個 byte 來表示,扣除掉 0x10000 後,剩下的點位就是該字的編碼

該字的 Unicode 點位為: 0x20380 - 0x10000 = 0x10380,也就是二進位 0001000000 1110000000 將其切割為兩部分,先拿低點位的 10 碼,剩下高點位不足的部分補 0,如此一來就會得到兩個 10 位元組的碼位,

  • 高點位:0001000000
  • 低點位:1110000000

輔助平面的高低點位初始值為 0xD8000xDC00,所以將剛剛的計算出來的點位加上這兩個值就是該字的編碼

這個初始值,轉為二進位後有 6 碼,再加上切割出來的 10 碼,剛好就是一個 16 位元組的字元

高點位的計算為 0xD800,也就是 110110 0000000000,加上剛剛切割出來的高位十碼 = 110110 0001000000 = 0xD840
低點位的計算為 0xDC00,也就是 110111 0000000000,加上剛剛切割出來的低位十碼 = 110111 1110000000 = 0xDF80

為了要表示這一個字將其轉為二進位後,總共使用了 4 個 byte,也就是 16 個位元組 11011000 01000000 11011111 10000000
所以 𠎀 的編碼為 0xD840 0xDF80

一般來說瞭解這種東西也沒有甚麼用,但如果有機會接觸到編碼問題,多知道一些細節對於偵錯會有幫助

ReCap

  1. Unicode 是把所有的字元都統一編碼,目的是為了解決不同語言之間的文字互通
  2. 在電腦上要處理文字,通常都採用 UTF8,因為更省空間
  3. UTF16 並沒有比 UTF8 更好,兩者差異只在於如何編碼 BMP 平面的字元,對輔助平面的字元,兩者都需要使用 4 bytes 來表示
  4. 中文難字、EMOJI 等字元,通常都是在輔助平面,所以需要使用 4 bytes 來表示,計算過程可參考上述的範例

延伸:字元計算

有的時候會需要限制使用者的輸入字元數,可能是因為 SMS 的字元限制,也可能是因為要開資料庫欄位長度的限制,都會需要計算字元數、或者是 byte 數,這時候就可以使用 JavaScript 的 TextEncoder 來計算字元所佔用的 byte 數

下列範例,取自IT Tools

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<c-card>
<c-input-text v-model:value="text" multiline placeholder="Your text..." rows="5" />

<div mt-5 flex>
<n-statistic label="Character count" :value="text.length" flex-1 />
<n-statistic label="Word count" :value="text === '' ? 0 : text.split(/\s+/).length" flex-1 />
<n-statistic label="Line count" :value="text === '' ? 0 : text.split(/\r\n|\r|\n/).length" flex-1 />
<n-statistic label="Byte size" :value="formatBytes(getStringSizeInBytes(text))" flex-1 />
</div>
</c-card>
</template>

當中的 Character count 就是使用者輸入的字元長度,直接使用 text.length 即可;Byte size 經由TextEncoder計算,就算使用者輸入了 emoji 也可以正確的計算使用的 byte 數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function getStringSizeInBytes(text: string) {
return new TextEncoder().encode(text).buffer.byteLength;
}

export function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) {
return "0 Bytes";
}

const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));

return `${Number.parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`;
}