LoginSignup
3
2

More than 1 year has passed since last update.

JavaScript textLength as halfWidth (半角としてのテキスト幅)

Last updated at Posted at 2022-08-02

JavaScript で半角幅としての文字列長を調べた件です。日本語のみ考慮してます。

課題

input type="text" の size や textarea の cols に文字数にて横表示幅を指定すると、これらは半角が前提なので、全角文字が入ると困った挙動になります。

<meta charset="utf-8">
<input type="text" value="ABC" size="3">
<input type="text" value="赤青黄色" size="4">
<textarea cols="4">赤青黄色</textarea>
<textarea style="white-space: nowrap" cols="4">赤青黄色</textarea>

inputtext.png

text は見切れますし、textarea は自動改行されます。自動改行阻止すると当然のように textarea も見切れます。
(プロポーショナルフォントなので実は ABC も微妙に足りませんが、大体は見えてるのでスルーで)

対応

全角1文字は半角の2文字としてカウントする処理があれば良さそうです。
具体的には、以下のようにカウント分けします。

種類 文字例 length as harf width
記号 +-~ 1 (x 3 文字 = 3)
アルファベット ABCXYZ 1 (x 6文字 = 6)
半角カナ アイウエオ 1 (x 5文字 = 5)
全角カタカナ アイウエオ 2 (x 5文字 = 10)
全角ひがらな いろは 2 (x 3文字 = 6)
漢字 赤青黄 2 (x 3文字 = 6)
異体字 葛󠄀城市 2 (x 3文字 = 6)
絵文字 😄 2
合成絵文字 👨‍👩‍👧‍👦 2

コードベース

大さっぱに以下の方針で行きます。

  • 半角、もしくは全角では無いと判断できる文字はカウント +1
  • それ以外は多分全角だろうからカウント +2
  • 更に必要に応じて特殊なコード対応。
function lengthAsHalfWidth(text) {
    let len = 0;
    for (const t of text) {
        const cp = t.codePointAt(0);
        if (半角もしくは全角ではないと判断できる文字) {
            len += 1;  // 半角
        } if else (特殊なコード) {
            // コードに応じた何かの処理
        } else {
            len += 2;  // きっと全角
        }
    }
    return len;
}

ちなみに以下のようにするとうまく行きません。絵文字の章で説明します。

    for (let i = 0; i < text.length; i++) {
        const cp = text[i].codePointAt(0);

あと、charCodeAt を使うとコードが 2 byte に収まらない場合(例えば異体字セレクタ 0xE0100)、2つに分かれた値を見て判断する必要があり面倒です。このエントリでは、codePointAt を使います。

> for (c of Array.from("葛󠄀")) { console.log(c.charCodeAt(0), c.charCodeAt(1)) }
33883 NaN
56128 56576
> for (c of Array.from("葛󠄀")) { console.log(c.codePointAt(0)) }
33883
917760

917760 は16進数だと 0xE0100 で、異体字セレクタです。

ASCII 記号/数字/アルファベット

if ((0x00 <= cp) && (cp < 0x7f)) {
    len += 1;  // ASCII 記号/数字/アルファベット
} else { 
...

0x20(スペース)〜 0x7E"~" の範囲に絞っても良いですが、それをしても範囲外(だいたい制御文字)が全角としてカウントされるだけなので。いっそのこと ASCII の前半を全てカバーで。

ASCII コードを 8bit のまま 0xA1 〜 0xDF を使って日本語のカタカナや記号を表す JIS X 0201 規格がありますが、Unicode に対応する領域があると聞かないので、スルーします。

半角カナ

SJIS で多用していた半角カナは Unicode にもあります。

if ((0xff61 <= cp) && (cp < 0xffa0)) {
    len += 1;  // 半角カナ
} else { 
...

半角カナは、昔の 2ch AAや、あとガラケー時代の文字を再現するのに必須ですね。。(๑>◡<๑)

SJIS半角予約領域

Unicode には私的利用領域があり、いわゆる外字を割り当てるのですが、SJIS に半角文字として将来(大昔にとっての将来)使うかもしれない予約領域があり、それとのマッピングを意識する場合、以下のようになります。

if ((0xf8f0 <= cp) && (cp < 0xf8f4)) {
    len += 1;  // 半角文字の予約エリア
} else { 
...

概念的には対応した方がすっきりするかもしれません。自分は一旦無視します。(テスト出来ないので)
実際にこの文字を見た事がある。という方がいれば、教えてください。

異体字

> Array.from("葛󠄀城市")
(4) ['', '󠄀', '', '']

この2つ目は異体字セレクタ(0xE0100)で、文字幅に影響しない為、無視します。

if (((0xfe00 <= cp) && (cp <= 0xfe0f)) ||
    ((0xe0100 <= cp) && (cp <= 0xe01fe))) {
   ;  // 異体字セレクタは幅0扱い
} else { 
...

絵文字

冒頭で i でループする方法が駄目と触れましたが、
実際に文字列を配列として参照すると、絵文字は2文字に分割されます。サロゲートペアが原因です。

> t = "赤青黄"; for (let i=0; i < t.length; i++) { console.log(t[i]) }



> "😄👀🌹".length
6
> t = "😄👀🌹"; for (let i=0; i < t.length; i++) { console.log(t[i]) }






Iterator がサロゲートペアに対応しているので、for of を使うか、Array.from(text) を通せば配列アクセスで問題なくなります。

> for (const c of "😄👀🌹") { console.log(c) }
😄
👀
🌹
> t = Array.from("😄👀🌹"); for (let i=0; i < t.length; i++) { console.log(t[i]) }
😄
👀
🌹

以下のサイトの解説が詳しいので参考にどうぞ

絵文字修飾

異体字セレクタの絵文字版みたいなものです。
具体的にはこれ "👍🏻👍🏼👍🏽👍🏾👍🏿"

if ((0x1f3fb <= cp) && (cp <= 0x1f3ff)) {
     ;  // Emoji Modifier

合成文字

例えば、👨‍👩‍👧‍👦 のような合成絵文字は Array.from でも対応できません。

> "👨‍👩‍👧‍👦".length
11
> text = Array.from("👨‍👩‍👧‍👦"); for (const c of text) { console.log(c) }
👨

👩

👧

👦

空行が入っているのが気になります。

> "👨‍👩‍👧‍👦".length
11
> Array.from("👨‍👩‍👧‍👦")
(7) ['👨', '', '👩', '', '👧', '', '👦']

この絵文字のノリしろになっている '' は、Zero Width Joiner (ZWJ,0x200D) です。
なので、前の絵文字の分スペースを戻せば、辻褄は合いそうです。

} else if (cp === 0x200d) {  // ZWJ
    len -= 2;  //  合成絵文字のノリしろ
} else {

なお、正しく処理したい場合は、専用ライブラリを使うと良いでしょう。例えばこれとか。

まとめ

function lengthAsHalfWidth(text) {
    let len = 0;
    for (const c of text) {
        const cp = c.codePointAt(0);
        if ((0x00 <= cp) && (cp < 0x7f)) {
            len += 1;  // ASCII 記号/数字/アルファベット
        } else if ((0xff61 <= cp) && (cp < 0xffa0)) {
            len += 1;  // 半角カナ
        } else if (cp === 0x200d) {  // ZWJ
            len -= 2;  // 合成絵文字のノリしろ
        } else if (((0xfe00 <= cp) && (cp <= 0xfe0f)) ||
                   ((0xe0100 <= cp) && (cp <= 0xe01fe))) {
            ;  // 異体字セレクタは幅0扱い
        } else if ((0x1f3fb <= cp) && (cp <= 0x1f3ff)) {
            ;  // 絵文字修飾も幅0扱い
        } else {
            len += 2;  // きっと全角
        }
    }
    return len;
}

もし、ZWJ で巻き戻るのが全角だけでなく半角の可能性があるのなら、少し複雑ですが、こんな実装が思いつきます。

function lengthAsHalfWidth(text) {
    let len = 0;
    let width = 0
    for (const c of text) {
        const cp = c.codePointAt(0);
        if ((0x00 <= cp) && (cp < 0x7f)) {
            width = 1; // ASCII 記号/数字/アルファベット
        } else if ((0xff61 <= cp) && (cp < 0xffa0)) {
            width = 1;  // 半角カナ
        } else if (cp === 0x200d) {  // ZWJ
            width = -width;  // 合成絵文字のノリしろ
        } else if (((0xfe00 <= cp) && (cp <= 0xfe0f)) ||
                   ((0xe0100 <= cp) && (cp <= 0xe01fe))) {
            ;  // 異体字セレクタは幅0扱い
        } else if ((0x1f3fb <= cp) && (cp <= 0x1f3ff)) {
            ;  // 絵文字修飾も幅0扱い
        } else {
            width = 2;  // きっと全角
        }
        len += width;
    }
    return len;
}

テスト

> lengthAsHalfWidth("XYZ");
3
> lengthAsHalfWidth("漢字ですデス");
12
> lengthAsHalfWidth("😄😇");
4
> lengthAsHalfWidth("アイウエオ");
5
> lengthAsHalfWidth("👨‍👩‍👧‍👦");
2

追記

絵文字の異体字に対応していませんでした。課題で。
"👍🏻👍🏼👍🏽👍🏾👍🏿"

参考

3
2
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2