Dart: Runes class แปลงข้อความให้เป็นรหัสค่า Unicode

ปัจจุบันการใช้งานข้อมูลข้อความตัวอักษรที่ใช้งานกันในคอมพิวเตอร์ มือถือ อินเทอร์เน็ต ล้วนแต่ใช้ Unicode เป็นตัวเข้ารหัสตัวอักษร เพื่อให้แสดงตัวอักษรได้กว้างกว่า ASCII แบบเดิม ๆ ที่มีแค่ 256 ค่าเท่านั้น โดยการเข้ารหัสที่นิยมใช้จะมี 2 รูปแบบคือ
  • UTF-8 เป็นการเข้ารหัสที่ยังเข้ากับ ASCII ได้อยู่บางส่วน ทำให้ถ้าใช้ตัวอักษรภาษาอังกฤษ จะสามารถอ่านด้วยโปรแกรมรุ่นเก่า ๆ ได้
  • UTF-16 เป็นการเข้ารหัส 1 ตัวอักษร ใช้พื้นที่อย่างต่ำ 2 byte หรือ 16 bit

การแปลงข้อความของ String class ออกมาเป็นลำดับตัวอักษรของ Unicode (integer Unicode code points) ใน Dart จะมี Runes class ช่วยในเรื่องนี้

การสร้าง Runes class

ใช้คำสั่ง Runes() ในการสร้าง โดยระบุความที่ต้องการ

void main() {
  var r = Runes('abc');
  print(r); // output → (97, 98, 99)
}

จากตัวอย่างข้อความ abc เมื่อแปลงเป็น Runes แล้วจะได้ค่า 97 98 และ 99 ตามลำดับ แล้วตัวเลขพวกนี้มาจากไหน เมื่อลองไปดูในตาราง Unicode จะพบว่า มันคือค่า

  • a → U+006116 → 9710
  • b → U+006216 → 9810
  • c → U+006316 → 9910

อีกวิธีคือใช้คำสั่ง String.runes เพื่อคืนค่า Runes กลับมาก็ได้เช่นเดียวกัน

void main() {
  var r = 'abc'.runes;
  print(r); // output → (97, 98, 99)
}

การเข้าถึงสมาชิกใน Runes

Runes class มีการสืบทอดมากจาก Iterable class ดังนั้นการเข้าถึงสมาชิก จะใช้คำสั่งใน Iterable class ได้ทันที

void main() {
  var r = 'abc'.runes;
  print(r); // output → (97, 98, 99)
  print(r.first); // output → 97
  print(r.elementAt(0)); // output → 97
  print(r.last); // output → 99
  print(r.elementAt(2)); // output → 99

  for (int element in r) {
    print(element); // output → 97 98 99
  }
}

ประโยชน์และการใช้ Runes ในชีวิตจริง

ใน String class จะมีคำสั่ง .codeUnits ที่ใช้ดึงค่า UTF-16 code units ของ String อยู่แล้ว ถ้าทดสอบโดยกับข้อความทั่วไป พบว่าค่าที่ได้ไม่ได้แตกต่างค่าที่ออกมาจาก Runes Class เลย

const latinString = 'abc';
print(latinString.runes); // (97, 98, 99)
print(latinString.runes.first.toRadixString(16)); // output → 61 (this is U+0061 code for 'a')

for (final item in latinString.codeUnits) {
  print(item.toRadixString(10)); // 97 98 99 in decimal
  print(item.toRadixString(16)); // 61 62 63 in hexadecimal
}

จากตัวอย่าง จะเห็นว่า ค่าที่ได้จาก .runes เป็นค่า 97 98 และ 99 ในเลขฐาน 10 หรือ U+0061 U+0062 และ U+0063 ในเลขฐาน 16 ตามลำดับ ค่าเหล่านี้มีค่าเดียวกับค่าที่ได้จาก .codeUnits ที่เป็นแบบนี้เพราะ String เป็นชุด UTF-16 ถ้าหากตัวอักษรที่เก็บ มีค่าไม่เกิน U+FFFF มันจะเก็บแค่ 1 ช่อง หรือ 1 codeUnit เท่านั้น แต่เมื่อไหร่ที่มันหลุดจากช่วงนี้ มันจะเก็บเป็น 2 ช่อง

หากใช้เก็บภาษาไทย ซึ่งชุดตัวอักษรจะอยู่ในช่วง U+0E00 ถึง U+0E7F จะได้ผลดังนี้

ตัวอย่างตาราง Unicode ของ ภาษาไทย
const thaiString = 'ก๒'; // U+0E01 U+0E52
print(thaiString.runes); // → (3585, 3666)
print(thaiString.runes.first.toRadixString(16)); // output → e01 (this is U+0E01 code for 'ก')

for (final item in thaiString.codeUnits) {
  print(item.toRadixString(10)); // → 3585 and 3666 in decimal
  print(item.toRadixString(16)); // → e01 and e52 in hexadecimal
}

มาลองดูตัวอย่าง หากตัวอักษรชุดตั้งแต่ U+10000 จะเกิดอะไรขึ้น ในตารางชุดอักษร Linear B Syllabary จะเป็นชุดที่เริ่มต้น U+10000

ตัวอย่างตาราง Unicode ของ Linear B Syllabary

เมื่อลองเขียนคำสั่งสร้างข้อความและดูว่า Dart เก็บค่าเหล่านี้อย่างไร

const SyllabaryString = '\u{10000}';
print(SyllabaryString.runes); // → (65536)
print(SyllabaryString.runes.first.toRadixString(16)); // → 10000 (same as U+10000)

// Surrogate pairs:
for (final item in SyllabaryString.codeUnits) {
  print(item.toRadixString(16)); // → d800 dc00
}

จากตัวอย่างจะเห็นว่า เมื่อเก็บ Unicode รหัส U+10000 หากดูค่าใน runes จะแสดงค่าถูกต้อง แต่เมื่อไปดึงค่าใน codeUnits จะเห็นว่า Dart เก็บค่านี้ไว้ 2 ชุดต่อ 1 ตัวอักษรคือ 0xD800 กับ 0xDC00

สรุปว่าตัว Runes class จะช่วยจัดการแปลงการค่าของ String ที่เก็บในแบบ UTF-16 ที่เกิน 1 ชุดขึ้นไป มาเป็นรหัสตามตาราง Unicode ที่ถูกต้อง ดังนั้น หากต้องการอ้างถึงรหัส Unicode ของ String ที่อาจมีตัวอักษรชุดเพิ่มเติม เช่น Emoji Emoticons สัญลักษณ์ทางคณิตศาสตร์ ฯลฯ

สามารถดู Unicode block ทั้งหมดได้ที่ Wikipedia