Trong số các tính năng mới được ship cùng Ruby 2.4 là hỗ trợ Unicode cải tiến. Cụ thể, các phương thức như upcase và downcase hoạt động như mong đợi, chuyển "ä" thành "Ä" và ngược lại. Điều này làm tôi tò mò: những cải tiến Unicode nào khác đã được làm kể từ 2013 khi tôi đọc bài blog của André Arko "Strings in Ruby are UTF-8 now… right??"
Tôi đã test tất cả phương thức chuỗi của Ruby, không tìm lỗi kỹ thuật mà tìm vi phạm "nguyên tắc least surprise." Cụ thể, các giả định của tôi là:
- Ký tự duy nhất là duy nhất: "e" và "ë" khác nhau, giống như "e" và "E" là khác nhau.
- Ký tự đơn đếm như một ký tự, không quan tâm chúng được biểu diễn như thế nào trong unicode. Nghĩa là "e" và "ë" mỗi cái là một ký tự đơn, dù cái sau được biểu diễn bằng hai code points.
- Ký tự là bất biến. Đảo ngược chuỗi ký tự không nên thay đổi các ký tự riêng lẻ.
- Whitespace được xử lý như whitespace. Ngay cả những ký tự whitespace Unicode khó xử.
- Chữ số được xử lý như chữ số. Số 2 luôn là số 2 không quan tâm nó được viết như thế nào.
Thật đáng tiếc, hầu hết phương thức thao tác chuỗi của Ruby thất bại các test này. Nếu bạn đang làm việc với chuỗi Unicode, bạn do đó phải cực kỳ cẩn thận về những phương thức bạn dùng.
NOTE: Sau khi xuất bản, một số độc giả chỉ ra rằng nhiều thất bại tôi đề cập sẽ không xảy ra nếu tôi đã normalize chuỗi test unicode. Điều này đúng. Tuy nhiên chuỗi không được normalize tự động bởi Ruby hoặc Rails (trong bất kỳ app nào tôi test). Các test này luôn nhằm minh họa worst-case và tôi nghĩ chúng vẫn hữu ích trong aspect đó.
Test Unicode với Ruby 2.4.0
| Phương thức | Test | Mong đợi | Kết quả | Phán định |
|---|---|---|---|---|
| #% | "%s" % "noël" |
"noël" |
"noël" |
OK |
| #* | "noël" * 2 |
"noëlnoël" |
"noëlnoël" |
OK |
| #<< | "noël" << "ë" |
"noëlë" |
"noëlë" |
OK |
| #<=> | "ä" <=> "z" |
-1 |
-1 |
OK |
| #== | "ä" == "ä" |
true |
true |
OK |
| #=~ | "ä" =~ /a./ |
nil |
0 |
Cẩn thận! |
| #[] | "ä"[0] |
"ä" |
"a" |
Cẩn thận! |
| #[]= | "ä"[0] = "u" |
"u" |
"u" |
OK |
| #b | "ä".b.encoding.to_s |
"ASCII-8BIT" |
"ASCII-8BIT" |
OK |
| #bytes | "ä".bytes |
[97, 204, 136] |
[97, 204, 136] |
OK |
| #bytesize | "ä".bytesize |
3 |
3 |
OK |
| #byteslice | "ä".byteslice(1) |
"\xCC" |
"\xCC" |
OK |
| #capitalize | "ä".capitalize |
"Ä" |
"Ä" |
OK |
| #casecmp | "äa".casecmp("äz") |
-1 |
-1 |
OK |
| #center | "ä".center(3) |
" ä " |
"ä " |
Cẩn thận! |
| #chars | "ä".chars |
["ä"] |
["a", "̈"] |
Cẩn thận! |
| #chomp | "ä ".chomp |
"ä" |
"ä" |
OK |
| #chop | "ä".chop |
"" |
"a" |
Cẩn thận! |
| #chr | "ä".chr |
"ä" |
"a" |
Cẩn thận! |
| #clear | "ä".clear |
"" |
"" |
OK |
| #codepoints | "ä".codepoints |
[97, 776] |
[97, 776] |
OK |
| #concat | "ä".concat("x") |
"äx" |
"äx" |
OK |
| #count | "ä".count("a") |
0 |
1 |
Cẩn thận! |
| #crypt | "123".crypt("ää") == "123".crypt("aa") |
false |
false |
OK |
| #delete | "ä".delete("a") |
"ä" |
"̈" |
Cẩn thận! |
| #downcase | "Ä".downcase |
"ä" |
"ä" |
OK |
| #dump | "ä".dump |
"\"a\\u0308\"" |
"\"a\\u0308\"" |
OK |
| #each_byte | "ä".each_byte.to_a |
[97, 204, 136] |
[97, 204, 136] |
OK |
| #each_char | "ä".each_char.to_a |
["ä"] |
["a", "̈"] |
Cẩn thận! |
| #each_codepoint | "ä".each_codepoint.to_a |
[97, 776] |
[97, 776] |
OK |
| #each_line | "ä".each_line.to_a |
["ä"] |
["ä"] |
OK |
| #empty? | "ä".empty? |
false |
false |
OK |
| #encode | "ä".encode("ASCII", undef: :replace) |
"a?" |
"a?" |
OK |
| #encoding | "ä".encoding.to_s |
"UTF-8" |
"UTF-8" |
OK |
| #end_with? | "ä".end_with?("ä") |
true |
true |
OK |
| #eql? | "ä".eql?("a") |
false |
false |
OK |
| #force_encoding | "ä".force_encoding("ASCII") |
"a\xCC\x88" |
"a\xCC\x88" |
OK |
| #getbyte | "ä".getbyte(2) |
136 |
136 |
OK |
| #gsub | "ä".gsub("a", "x") |
"ä" |
"ẍ" |
Cẩn thận! |
| #hash | "ä".hash == "a".hash |
false |
false |
OK |
| #include? | "ä".include?("a") |
false |
true |
Cẩn thận! |
| #index | "ä".index("a") |
nil |
0 |
Cẩn thận! |
| #replace | "ä".replace("u") |
"u" |
"u" |
OK |
| #insert | "ä".insert(1, "u") |
"äu" |
"aü" |
Cẩn thận! |
| #inspect | "ä".inspect |
"\"ä\"" |
"\"ä\"" |
OK |
| #intern | "ä".intern |
:ä |
:ä |
OK |
| #length | "ä".length |
1 |
2 |
Cẩn thận! |
| #ljust | "ä".ljust(3, "_") |
"ä__" |
"ä_" |
Cẩn thận! |
| #lstrip | " ä".lstrip |
"ä" |
"ä" |
OK |
| #match | "ä".match("a") |
nil |
# |
Cẩn thận! |
| #next | "ä".next |
"ä" |
"b̈" |
Cẩn thận! |
| #ord | "ä".ord |
97 |
97 |
OK |
| #partition | "händ".partition("a") |
["händ"] |
["h", "a", "̈nd"] |
Cẩn thận! |
| #prepend | "ä".prepend("ä") |
"ää" |
"ää" |
OK |
| #replace | "ä".replace("ẍ") |
"ẍ" |
"ẍ" |
OK |
| #reverse | "händ".reverse |
"dnäh" |
"dn̈ah" |
Cẩn thận! |
| #rpartition | "händ".rpartition("a") |
["händ"] |
["h", "a", "̈nd"] |
Cẩn thận! |
| #rstrip | "line ".rstrip |
"line" |
"line " |
Cẩn thận! |
| #scrub | "ä".scrub |
"ä" |
"ä" |
OK |
| #setbyte | s = "ä"; s.setbyte(0, "x".ord); s |
"ẍ" |
"ẍ" |
OK |
| #size | "ä".size |
1 |
2 |
Cẩn thận! |
| #slice | "ä".slice(0) |
"ä" |
"a" |
Cẩn thận! |
| #split | "ä".split("a") |
["ä"] |
["", "̈"] |
Cẩn thận! |
| #squeeze | "ää".squeeze("ä") |
"ä" |
"ää" |
Cẩn thận! |
| #start_with? | "ä".start_with?("a") |
false |
true |
Cẩn thận! |
| #strip | " line ".strip |
"line" |
" line " |
Cẩn thận! |
| #sub | "ä".sub("a", "x") |
"ä" |
"ẍ" |
Cẩn thận! |
| #succ | "ä".succ |
"b̈" |
"b̈" |
OK |
| #swapcase | "ä".swapcase |
"Ä" |
"Ä" |
OK |
| #to_c | "١".to_c |
(1+0i) |
(0+0i) |
Cẩn thận! |
| #to_f | "١".to_f |
1.0 |
0.0 |
Cẩn thận! |
| #to_i | "١".to_i |
1 |
0 |
Cẩn thận! |
| #to_r | "١".to_r |
(1/1) |
(0/1) |
Cẩn thận! |
| #to_sym | "ä".to_sym |
:ä |
:ä |
OK |
| #tr | "ä".tr("a", "b") |
"ä" |
"b̈" |
Cẩn thận! |
| #unpack | "ä".unpack("CCC") |
[97, 204, 136] |
[97, 204, 136] |
OK |
| #upto | "ä".upto("c̈").to_a |
["ä", "b̈", "c̈"] |
["ä", "b̈", "c̈"] |
OK |
| #valid_encoding? | "ä".valid_encoding? |
true |
true |
OK |