c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

JavaScript で、文字列を UTF-8 にした時の byte 數を計算する

Ruby

irb> '冬'.bytes.map{ |n| n.to_s(16) }
=> ["f0", "af", "a0", "9a"]
irb> '冬'.bytesize
=> 4
irb> '羽󠄀'.bytes.map{ |n| n.to_s(16) }
=> ["e7", "be", "bd", "f3", "a0", "84", "80"]
irb> '羽󠄀'.bytesize
=> 7

冬の正字冬"\u{2f81a}"で 4 byte、(Prettier の bug (直した。Correctly format CJK sentences with Variation Selector. by ne-sachirou · Pull Request #8511 · prettier/prettier) で本文中に正しく書けないが) 羽の正字"\u7fbd\u{e0100}"で 7 byte だ。

Ruby の標準 encoding は UTF-8 だからそのまま byte 數を數へれば好い。

TypeScript

追記 2020-10-16

非標準だが unescape 函數を使ふ方法が有ると教えて頂いた。

cf. 文字列と UTF-8 バイト列の相互変換: Days on the Moon

console.info("羽󠄀".length);
// 3

console.info(unescape(encodeURIComponent("羽󠄀")).split(""));
// [
//   'ç',    '¾',
//   '½',    'ó',
//   ' ',    '\x84',
//   '\x80'
// ]

console.info(unescape(encodeURIComponent("羽󠄀")).length);
// 7

類似の方法として UTF-8 に變換した長さを知るだけなら以下も有るらしい。encodeURIComponent 函數が文字列を UTF-8 として扱ってくれるからだ。

console.info(encodeURIComponent("羽󠄀").replace(/%../g, "_").length);
// 7

又新しい API として TextEncoder が有ると教わった。Internet Exploler 以外ではもう使へる。

TextEncoder - Web API | MDN

console.info(new TextEncoder().encode("羽󠄀"));
// Uint8Array(7) [
//   231, 190, 189,
//   243, 160, 132,
//   128
// ]

JavaScript の内部 encoding は UTF-16 だからこれを UTF-8 に變換する。Unicode の code point そのものである UTF-32 に變換し、そこから UTF-8 にする。

const isHighSurrogate = (codePoint: number): boolean =>
  0xd800 <= codePoint && codePoint <= 0xdbff;

const isLowSurrogate = (codePoint: number): boolean =>
  0xdc00 <= codePoint && codePoint <= 0xdfff;

const utf16ToUtf32 = (str: string): Array<number> => {
  const utf32Str: Array<number> = [];
  let highSurrogate: number | undefined;
  for (const ch of str.split("")) {
    const codePoint = ch.codePointAt(0);
    if (highSurrogate) {
      // if (!isLowSurrogate(codePoint)) {
      //   throw new Error(`Surrogate pair not closed: ${codePoint.toString(16)}`);
      // }
      utf32Str.push(
        0x10000 + (highSurrogate - 0xd800) * 0x400 + (codePoint - 0xdc00)
      );
      highSurrogate = undefined;
    } else if (isHighSurrogate(codePoint)) {
      highSurrogate = codePoint;
    } else {
      utf32Str.push(codePoint);
    }
  }
  // if (highSurrogate) {
  //   throw new Error("Surrogate pair not closed");
  // }
  return utf32Str;
};

const utf32ToUtf8 = (utf32Str: Array<number>): Array<number> => {
  const utf8Str: Array<number> = [];
  for (const utf32Ch of utf32Str) {
    // if (utf32Ch < 0 || 0x10ffff < utf32Ch) {
    //   throw new Error(`Not a UTF-32 char: ${utf32Ch.toString(16)}`);
    // }
    if (utf32Ch <= 0x7f) {
      utf8Str.push(utf32Ch);
    } else if (utf32Ch <= 0x7ff) {
      utf8Str.push(0xc0 | (utf32Ch >> 6));
      utf8Str.push(0x80 | (utf32Ch & 0x3f));
    } else if (utf32Ch <= 0xffff) {
      utf8Str.push(0xe0 | (utf32Ch >> 12));
      utf8Str.push(0x80 | ((utf32Ch >> 6) & 0x3f));
      utf8Str.push(0x80 | (utf32Ch & 0x3f));
    } else {
      utf8Str.push(0xf0 | (utf32Ch >> 18));
      utf8Str.push(0x80 | ((utf32Ch >> 12) & 0x3f));
      utf8Str.push(0x80 | ((utf32Ch >> 6) & 0x3f));
      utf8Str.push(0x80 | (utf32Ch & 0x3f));
    }
  }
  return utf8Str;
};

const utf8Length = (str: string): number =>
  utf32ToUtf8(utf16ToUtf32(str)).length;

これで、

console.info(
  "UTF-16",
  "冬".split("").map((c) => c.codePointAt(0).toString(16))
);
console.info(
  "UTF-32",
  utf16ToUtf32("冬").map((n) => n.toString(16))
);
console.info(
  "UTF-8",
  utf32ToUtf8(utf16ToUtf32("冬")).map((n) => n.toString(16))
);
console.info(utf8Length("冬"));

console.info(
  "UTF-16",
  "羽󠄀".split("").map((c) => c.codePointAt(0).toString(16))
);
console.info(
  "UTF-32",
  utf16ToUtf32("羽󠄀").map((n) => n.toString(16))
);
console.info(
  "UTF-8",
  utf32ToUtf8(utf16ToUtf32("羽󠄀")).map((n) => n.toString(16))
);
console.info(utf8Length("羽󠄀"));
UTF-16 [ 'd87e', 'dc1a' ]
UTF-32 [ '2f81a' ]
UTF-8 [ 'f0', 'af', 'a0', '9a' ]
4
UTF-16 [ '7fbd', 'db40', 'dd00' ]
UTF-32 [ '7fbd', 'e0100' ]
UTF-8 [
  'e7', 'be',
  'bd', 'f3',
  'a0', '84',
  '80'
]
7

cf. UTF-32、UTF-16、UTF-8 の相互変換 - Qiita

Scala

JVM の内部 encoding は UTF-16 だが、簡單に encoding を變換出來る。

scala> "冬".getBytes("UTF-8").map("%02x".format(_))
res: Array[String] = Array(f0, af, a0, 9a)
scala> "冬".getBytes("UTF-8").length
res: Int = 4
scala> "羽󠄀".getBytes("UTF-8").map("%02x".format(_))
res: Array[String] = Array(e7, be, bd, f3, a0, 84, 80)
scala> "羽󠄀".getBytes("UTF-8").length
res: Int = 7

PostgreSQL

example=# select octet_length('冬');
 octet_length
--------------
            4
(1 row)

example=# select octet_length('羽󠄀');
 octet_length
--------------
            7
(1 row)