5920


JavaScriptで絵文字の正確な長さを取得したいのですが、うまくいきません。  
(蛇の絵文字)は1文字なので 1 を期待しているのですが、2 が 返ってきてしまいます。  
Firefox, Node.js, io.js などいろいろなもので試しましたが、やはり 2 が返ってきています。  
どうすれば、正確な長さを取得できるのでしょうか?

[code]

    
    var my_string = ""; // 蛇の絵文字
    var str_length = my_string.length;
    console.log(str_length); // 2
    
    
[/code]


CC BY-SA 3.0
2015-02-04T04:53:36.743
5918
2016-09-01T01:53:39.510
5246
question
javascript node.js firefox
JavaScriptで絵文字の正確な長さを取得する


絵文字はサロゲートペアと呼ばれる ~~領域に格納されています~~ 代用符号を用いた2個組の文字で構成されます。  
サロゲートペアを間引いた数が可視文字の数となります。

[code]

    var my_string = "";
    var str_length = my_string.length - (my_string.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g).length - 1);
    print(str_length); // 1
    
[/code]

**追記**

ちょっと改良

[code]

    var my_string = "";
    var str_length = my_string.length - (my_string.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g)||[]).length;
    print(str_length); // 1
    
[/code]


CC BY-SA 3.0
2015-02-04T04:59:46.793
5920
2015-02-04T06:57:13.613
2015-02-04T06:57:13.613
440
440
5918
answer


UTF-16
では、[サロゲートペア](https://ja.wikipedia.org/wiki/Unicode#.E3.82.B5.E3.83.AD.E3.82.B2.E3.83.BC.E3.83.88.E3.83.9A.E3.82.A2)を使う文字、例えば件の
"" は４バイトの表現となり、 `length` プロパティでは 2 文字であると認識されてしまいます。

_`toHexUnicode()`: 文字コードを調べる関数_:

[code]

    function toHexUnicode( str) {
        var codes = "";
        for ( i = 0; i < str.length; i++) {
            var codeHex = str.charCodeAt(i).toString(16).toUpperCase();
            codes = codes + "U+" + codeHex;
        }
        return codes;
    }
    
[/code]

この関数を使って調べると、"" が4バイトであることが分かります。

[code]

    toHexUnicode("")
    // => "U+D83DU+DC0D"
    
[/code]

対策ですが、この際、文字列に新しいプロパティを定義してしまってはどうでしょうか。

[code]

    String.prototype.__defineGetter__("length_unicode", function() {
      var i = 0, len = this.length, result = 0;
      while (i < len) {
        result++;
        var x = this.charCodeAt(i++);
        if (0xD800 <= x && x < 0xDC00) i++;
      }
      return result;
    });
    
[/code]

`length_unicode` のテスト:

[code]

    my_string.length
    // => 2
    my_string.length_unicode
    // => 1
    
[/code]

* * *

文字数のカウントの仕方については、、以下の記事を参考にしました。同様のことは、参考リンク中の [js-
codepoints](https://github.com/dankogai/js-codepoints) ライブラリで可能です。

  * [Twitter時代の文字の数え方](http://blog.unfindable.net/archives/2728), 配電盤
  * [javascript - でBMP以外のUnicode文字をきちんと扱う](http://blog.livedoor.jp/dankogai/archives/51861512.html), 404 Blog Not Found

記事にあるとおり、"野家"なども問題になるんですね。

[code]

    toHexUnicode("")
    // => "U+D842U+DFB7"
    toHexUnicode("野家")
    // => "U+D842U+DFB7U+91CEU+5BB6"
    "野家".length_unicode
    // => 3
    "野家".length
    // => 4
    
[/code]


CC BY-SA 3.0
2015-02-04T05:44:26.247
5922
2015-02-04T05:44:26.247
4978
5918
answer


ES6だと

[code]

    str_length = [...my_string].length
    
[/code]

で取れます。


CC BY-SA 3.0
2015-02-05T05:11:28.513
5973
2015-02-05T05:11:28.513
5721
5918
answer


絵文字の中には国コードの組み合わせた国旗、バリエーションセレクターが後ろに続く異体字 (バリアント)、肌色を変える修飾子が続くもの (Unicode 8.0
で導入予定)
など、2つのコードポイントで構成されるものもあるので、それらも考慮する必要がある場合、拡張書記素クラスターに対応したライブラリが必要になります。grapheme-
breaker のコードの例を示します。

[code]

    var GraphemeBreaker = require('grapheme-breaker');
    
    // 日本の国旗
    // http://www.taishukan.co.jp/kokugo/webkoku/series003_11.html
    var c = String.fromCodePoint(0x1F1EF, 0x1F1F5);
    console.log(c);
    
    // バリアント
    // http://unicode.org/Public/UNIDATA/StandardizedVariants.html
    var c2 = String.fromCodePoint(0x267F, 0xFE0F);
    console.log(c2);
    
    // Unicode 8.0 で対応予定
    // VI 型の肌色の少年
    // http://www.unicode.org/reports/tr51/tr51-2.html#Diversity
    // var c3 = String.fromCodePoint(0x1F466, 0x1F3FF);
    // console.log(c3);
    
    var str = c + c2;
    // 拡張書記素クラスターの数
    console.log(GraphemeBreaker.countBreaks(str));
    
    var length = str.length;
    var previous = 0;
    var current = 0;
    
    while (current < length) {
        previous = current;
        current = GraphemeBreaker.nextBreak(str, previous);
        console.log(str.slice(previous, current));
    }
    
[/code]

文字数のバリデーションを扱う際に、1つの拡張書記素クラスターを構成する要素数の上限がないことを考慮する必要があります。

[code]

    var c = "\u267F" + "\uFE0F".repeat(10000);
    console.log(1 === GraphemeBreaker.countBreaks(c));
    
[/code]

Chrome や Intl を有効にした node/iojs
であれば、[Intl.v8BreakIterator](https://code.google.com/p/v8-i18n/wiki/BreakIterator)
を使うことができます。

[code]

    var c = String.fromCodePoint(0x1F1EF, 0x1F1F5);
    var c2 = String.fromCodePoint(0x267F, 0xFE0F);
    var str = c + c2;
    
    console.log(2 === graphemeLength(str, ['ja-JP']));
    
    function graphemeLength(str, locales)
    {
        var it = new Intl.v8BreakIterator(locales, {type: 'character'});
        it.adoptText(str);
    
        var length = 0;
        var previous = 0;
        var current = it.first();
    
        while (current !== -1) {
    
            current = it.next();
    
            if (current === -1) {
                break;
            }
    
            previous = current;
            ++length;
        }
    
        return length;
    }
    
[/code]


CC BY-SA 3.0
2015-03-08T12:56:42.407
7615
2015-03-12T11:33:19.583
2015-03-12T11:33:19.583
568
568
5918
answer


`String.prototype[@@iterator]` はサロゲートペアが考慮されているので、  
それを内部で使用している関数など使えば取得できます。

[code]

    Array.from("野家").length
    // => 3
    
[/code]


CC BY-SA 3.0
2016-09-01T01:53:39.510
28614
2016-09-01T01:53:39.510
2475
5918
answer