在Java和JavaScript中,我們經常會使用字串(String)提供的length
成員(member)來抓取這個字串的字數。由於Java和JavaScript並不像Rust或是Golang程式語言這樣會直接抓到字串在經過編碼之後的位元組數量,而是會得到字元的數量,所以我們很直覺地就會在Java或JavaScript用這樣的方式來計算字串的字數。
例如以下的計算字數
函數,好像就可以成功正確計算出字數了啊?
Java
TypeScript
// 不要用,這是爛Code!
public static int 計算字數(final String s) {
return s.length();
}
// 不要用,這是爛Code!
const 計算字數 = (s: string): number => {
return s.length;
};
實測看看:
Java
TypeScript
System.out.println(計算字數("abc123")); // 6
System.out.println(計算字數("中文字")); // 3
System.out.println(計算字數(" ")); // 1
System.out.println(計算字數(" ")); // 1
System.out.println(計算字數("哈囉 world")); // 8
console.log(計算字數("abc123")); // 6
console.log(計算字數("中文字")); // 3
console.log(計算字數(" ")); // 1
console.log(計算字數(" ")); // 1
console.log(計算字數("哈囉 world")); // 8
字數正確啊?有什麼問題嗎?
先別急著肯定,再多試試,程式如下:
Java
TypeScript
System.out.println(計算字數("𩸽")); // 2
System.out.println(計算字數("𡇙")); // 2
System.out.println(計算字數("😮")); // 2
console.log(計算字數("𩸽")); // 2
console.log(計算字數("𡇙")); // 2
console.log(計算字數("😮")); // 2
𩸽
讀「ㄌㄨㄥˇ」;𡇙
讀「ㄉㄨㄛˇ」;😮
是驚訝表情。這些東西應該都只能分別算是一個字,但是使用以上的計算字數
函數卻都被計算為兩個字了。
這是因為Java和JavaScript是使用UTF-16來編碼字串,一個字可能會被編碼為2個位元組(byte)或是4個位元組。但Java和JavaScript都認為一個字元(character)是16個位元(即2個位元組),而字串"𩸽"
、"𡇙"
、"😮"
在經過UTF-16編碼後都是4個位元組,就會被當成是2個字元。Java和JavaScript的字串提供的length
成員能夠取得字元數量。
JavaScript並未直接提供「字元」型別;Java則有提供,即char
。在Java中,無法直接將UTF-16編碼後為4個位元組的字當作一個char
型別的資料來使用,例如以下這行Java敘述就會造成編譯錯誤:
char c = '𩸽';
所以說,在Java和JavaScript中直接將字元數量視同字串的字數是不安全 的!
安全地計算字數
筆者最先想到的方式還是先把字串進行UTF-8的編碼後再去計算字數。
有關於UTF-8字元寬度的說明可以參考下面這篇文章(Rust程式語言):
Java
TypeScript
public static int 計算字數(final String s) {
final byte[] utf8Data;
try {
utf8Data = s.getBytes("UTF-8");
} catch (final Exception exception) {
// unreachable
return -1;
}
int p = 0;
int counter = 0;
while (p < utf8Data.length) {
final int first = Byte.toUnsignedInt(utf8Data[p]);
++counter;
if (first <= 0x7F) {
p += 1;
} else if (first <= 0xDF) {
p += 2;
} else if (first <= 0xEF) {
p += 3;
} else {
p += 4;
}
}
return counter;
}
Java雖然支援UTF-32編碼,可以直接把每個字元編碼成4個位元組,之後再把位元組的數量除以4就是字數了。不過這個作法效能並不好,不建議使用。參考用的程式碼如下:
// 不要用,這是爛Code!
public static int 計算字數(final String s) {
try {
return s.getBytes("UTF-32BE").length / 4;
} catch (final Exception exception) {
// unreachable
return -1;
}
}
const 計算字數 = (s: string): number => {
const utf8Data = new TextEncoder().encode(s);
let p = 0;
let counter = 0;
while (p < utf8Data.length) {
const first = utf8Data[p];
counter += 1;
if (first <= 0x7F) {
p += 1;
} else if (first <= 0xDF) {
p += 2;
} else if (first <= 0xEF) {
p += 3;
} else {
p += 4;
}
}
return counter;
};
或者也可以使用以下更推薦、更快速簡潔的做法:
Java
TypeScript
public static int 計算字數(final String s) {
return s.codePointCount(0, s.length());
}
const 計算字數 = (s: string): number => [...s].length;