JavaScriptの正規表現の戻り読みはPerlのそれよりも表現力が高い2018年12月29日 22時37分

ECMAScript 2018で正規表現の戻り読み(lookbehind)が追加されました。

/(?<=foo)bar/.test('foobar'); // => true
'foobar'.replace(/(?<=foo)bar/, 'baz'); // => 'foobaz

正規表現の戻り読みと言えばPerlでは1998年7月リリースのバージョン5.005からサポートしており、そこから20年もたってと思いたくなるかもしれません。しかし、ECMAScript (JavaScript)のそれはPerlのものとは一味違います。なんと戻り読みの中で量指定子(*+?{n}など)を使えるのです。

// JavaScriptなら(?<=...)の中で+が使える。
/(?<=fo+)bar/.test('foobar'); // => true
# Perlでは(?<=...)の中で+を使おうとするとエラーになる。
"foobar" =~ m/(?<=fo+)bar/;
# => Variable length lookbehind not implemented in regex m/(?<=fo+)bar/ at - line 1.

Perlの場合、戻り読みとして指定されたパターンの文字数だけ戻ってから(文字列末尾方向に向けて)マッチしていきます。あらかじめ戻る文字数がわからないといけないので、戻り読みの中で量指定子を使えません。

一方、ECMAScriptでは戻り読みとして指定されたパターンそのものを逆方向に解釈し、1文字ずつ逆向きに(文字列先頭方向に向けて)マッチしていきます。ですから量指定子が含まれていても問題ないのです。

パターンが逆方向に解釈されるので、後方参照を使えるようになるのも戻る方向になります。

// 1番目の(...)よりも\1のほうが文字列先頭方向側にある。
                                 //     $&     $1
/(?<=f\1(o))bar/.exec('foobar'); // => ["bar", "o"]

ただし、後方参照の番号はパターン内での出現順のままです。

                                              //     $&          $1     $2
/([0-9]+)([0-9]+)兆円/.exec('5000兆円');      // => ["5000兆円", "500", "0"  ]
/(?<=([0-9]+)([0-9]+))兆円/.exec('5000兆円'); // => ["兆円",     "5",   "000"]

知られてそうで知られてない少し知られてるECMAScript 2015 (ES6)の新機能2016年05月12日 15時38分

Kyoto.js #10で「知られてそうで知られてない少し知られてるECMAScript 2015 (ES6)の新機能」というライトニングトークをしたので、そのスライドに当日しゃべった内容や補足を追記して以下に掲載します。


自己紹介

ECMAScript 2015 (ECMA-262 6th Edtition)

  • クラス構文
  • アロー関数
  • ブロックスコープ
  • Promise
  • etc...

2015年にECMAScriptが大幅に改定されました。クラス構文やアロー関数の追加はあちこちで取り上げられているので皆さんご存知でしょうが、ここではあまり取り上げられていない地味な新機能を紹介します。

Unicodeの符号位置

// 🍣

'\uD83C\uDF63';  // ES5
'\u{1F363}';     // ES6

String.fromCharCode(0xD83C, 0xDF63);  // ES5
String.fromCodePoint(0x1F363);        // ES6

JavaScriptの文字列はUTF-16符号化形式で表現されます。絵文字などBMP(基本多言語面)外の文字はサロゲートペアと呼ばれるふたつの符号単位の組み合わせで表現されるため、これまでエスケープシーケンスを使うときなどには符号単位(16ビット符号なし整数)ごとに指定する必要がありました。ES2015ではUnicodeの符号位置(U+0U+10FFFFまでの値)を直接指定できるようになります。

Unicode正規化

'㍿ 100パーセント'.normalize('NFKC');
// => '株式会社 100パーセント'

'神 森鷗外'.normalize('NFC')
// => '神 森鷗外'

Unicodeには結合文字などの表現形式をそろえる「正規化」という手順が定義されています。ES2015では文字列にnormalizeメソッドが追加され、NFCやNFKCといった複数種類の正規化を適用できるようになりました。

NFKCで文字列を正規化すると、組文字が個々の文字に展開されたり、全角英数字が半角に、半角カタカナが全角にそろえられたりします。正規化を行うことで「神」が「神」になるなど字体が変わってしまうこともあるので注意してください。

正規表現の/uフラグ(unicodeオプション)

/^..$/.test('🍣');  // => true
/^.$/u.test('🍣');  // => true

/\p/.test('p');   // => true
/\p/u.test('p');  // => SyntaxError

これまで正規表現は符号単位ごとにマッチしていましたが、/uフラグを指定することで符号位置ごとにマッチするようになります。

また、未定義のメタ文字があったとき、これまでは\が無視されていましたが、/uフラグ下では構文エラーになります。これにより、将来新たなメタ文字を導入しても、既存の正規表現パターンの挙動が変わってしまうということがなくなります。将来の拡張性確保のためには重要な変更です。

正規表現の/yフラグ(stickyオプション)

let re = /bar/y;

re.test('foobar');  // => false

re.lastIndex = 3;
re.test('foobar');  // => true

通常正規表現マッチは部分一致ですが、/yフラグを付けると前方一致になります。/bar/.test('foobar')はマッチ成功となりますが/bar/y.test('foobar')barが先頭にないのでマッチ失敗となります。

前方一致といいましたが、より正確に言うと正規表現オブジェクトのlastIndexプロパティで指定した位置からの一致になります。上の例ではlastIndex3を指定したので、fooの直後からマッチを試みることになり、マッチが成功しています。

これが役に立つ場面として字句解析があげられます。文字列からトークンを切り出した後、次のトークンを探すときには前回のトークンの終了位置直後から探索できると便利でしょう。

正規表現の挙動の上書き

let regExpLike = {
    [Symbol.match]()   { return 42; },
    [Symbol.replace]() { return 23; },
    [Symbol.search]()  { return 12; },
};
'foo'.match(regExpLike);           // => 42
'foo'.replace(regExpLike, 'bar');  // => 23
'foo'.search(regExpLike);          // => 12

文字列のmatchメソッドなどは、引数として渡されたオブジェクトに処理を委譲するようになりました。Symbol.matchメソッドを持つオブジェクトを引数に渡すことで、そのメソッドの返り値がそのまま文字列のmatchメソッドの返り値となります。

RegExpを継承するオブジェクトなら、execメソッドを上書きするだけでも大丈夫です。

class MyRegExp extends RegExp {
   exec(str) { return [42]; }
}
let re = new MyRegExp;
'foo'.match(re); // => [42]

この挙動の変更は、XRegExpのような正規表現を拡張するライブラリの作成に有用だと思われます。

数学関数の追加

Math.sinh(1);   // => 1.1752011936438014
Math.expm1(1);  // => 1.718281828459045

双曲線関数や常用対数・二進対数関数など、多くの数学関数が追加されています。

Math.expm1(x)Math.exp(x) - 1と同じですが、xが0に近い値のときにより精度の高い結果を返します。

数値に限定した判別関数

isNaN('foo');         // => true
Number.isNaN('foo');  // => false

isFinite('42');         // => true
Number.isFinite('42');  // => false

従来のグローバル関数isNaN / isFiniteは引数を数値に変換してから非数・有限値かどうかを判定していました。ES2015で追加されたNumber.isNaN / Number.isFiniteは引数が数値でなければその時点で偽を返します。

整数の扱い

Math.trunc(3.1);   // => 3
Math.trunc(-3.1);  // => -3

Math.imul(0xFFFFFFFF, 0xFFFFFFFF);  // => 1

Number.isInteger(Math.pow(2, 53));      // => true
Number.isSafeInteger(Math.pow(2, 53));  // => false

Number.MAX_SAFE_INTEGER;
// => 9007199254740991 (2 ** 53 - 1)
Number.MIN_SAFE_INTEGER;
// => -9007199254740991 (- (2 ** 53 - 1))

Math.truncは引数が正の数なら切り下げ(Math.floor)を、負の数なら切り上げ(Math.ceil)を行います。

Math.imulはふたつの32ビット符号なし整数値を乗算し、その結果の下位32ビットの値を返します。単に*演算子を使うと64ビット浮動小数点数の有効桁数を超えたときに不正確な値になってしまいます。

整数かどうかの判定メソッドNumber.isIntegerも追加されました。Number.isSafeIntegerは整数であるかどうかに加えて、64ビット浮動小数点数で一の位まで正確に表せる範囲内かどうかも見ます。新たな定数を使って言うと、Number.MIN_SAFE_INTEGER以上Number.MAX_SAFE_INTEGER以下の整数に対してNumber.isSafeIntegerは真を返します。

completion valueの変化

eval('42; if (true) {}');  // ES5 => 42
eval('42; if (true) {}');  // ES6 => undefined

ECMAScript仕様では、式や文の評価結果(返り値や制御フロー)を表すcompletion recordという値が定義されています。これは概念的なものであり、各JavaScriptエンジンがcompletion recordという値を実装しているとは限りません。completion recordに基づく文の評価結果、いわば「文の返り値」は通常触れることはできませんが、eval関数の結果としてその値を確かめられます。

ES2015ではif文やfor文といった制御構文におけるcompletion recordの扱いが少し変更されました。ES5までは制御構文の本体が空だったとき、プログラム全体の返り値はその制御構文の直前の文の返り値を引き継いでいました。ES2015からは制御構文の本体が空ならundefinedが返るようになります。

この変更は主にJavaScriptエンジンの実装の都合によるものです。これまでは制御構文の評価に移ってもそれ以前の文の返り値を保持しておく必要がありました。ES2015に沿った実装なら制御構文の評価に移った時点でそれ以前の文の返り値を破棄してもよく、実装に最適化の余地が生まれると考えられます。

HTMLのようなコメント (付属書B: Webブラウザのための追加機能)

<!--
doSomething();
-->

HTMLのscript要素内にJavaScriptを書くときなど、要素の内容全体を<!-- -->で囲むことがあります。このとき<!---->も単一行コメントとみなされますが、この挙動はECMAScriptで定義されたものではなく、あくまで各Webブラウザの独自拡張という扱いでした。

ES2015では(仕様本文ではなく)付属書において<!-- -->をコメントとして扱う構文を定義しています。ただし、あくまでWebブラウザが後方互換性を確保するために実装するものであり、ブラウザと直接関係ない実装(Node.jsなど)がこの構文に対応することは推奨されていません。プログラマもこうしたレガシーな機能を使うべきでないとされています。

漢字にマッチする JavaScript の正規表現パターン2015年12月31日 23時03分

たまに漢字にマッチする正規表現パターンを書きたいときがあります。Perl の正規表現だと Unicode のスクリプト名を使って \p{Han} で漢字にマッチさせられるのですが、JavaScript ではそうはいきません。JavaScript の正規表現には以下のふたつの問題があります。

  1. Unicode スクリプト名の指定 (\p{...}) に対応していない。
  2. そもそも Unicode の符号位置に対してマッチさせられない (UTF-16 における符号単位に対するマッチになる)。

とはいえ、解決不能な問題というわけでもないので、Perl の \p{Han} を JavaScript に移植してみましょう。\p{Han} の範囲は以下のコマンドで参照できます。

perl -MUnicode::UCD -MData::Dumper -E 'say Dumper Unicode::UCD::charscript("Han");'

Perl 5.20.2 (Unicode 6.3.0) では以下の範囲が含まれるとわかりました。

  • U+2E80 - U+2E99
  • U+2E9B - U+2EF3
  • U+2F00 - U+2FD5
  • U+3005 - U+3005
  • U+3007 - U+3007
  • U+3021 - U+3029
  • U+3038 - U+303B
  • U+3400 - U+4DB5
  • U+4E00 - U+9FCC
  • U+F900 - U+FA6D
  • U+FA70 - U+FAD9
  • U+20000 - U+2A6D6 (\uD840\uDC00 - \uD869\uDED6)
  • U+2A700 - U+2B734 (\uD869\uDF00 - \uD86D\uDF34)
  • U+2B740 - U+2B81D (\uD86D\uDF40 - \uD86E\uDC1D)
  • U+2F800 - U+2FA1D (\uD87E\uDC00 - \uD87E\uDE1D)

サロゲートペアが [\uD800-\uDBFF][\uDC00-\uDFFF] であることを踏まえると、BMP 外の範囲はそれぞれ以下の部分的な範囲に展開できます。

  • \uD840\uDC00 - \uD869\uDED6
    • [\uD840-\uD868][\uDC00-\uDFFF]
    • \uD869[\uDC00-\uDED6]
  • \uD869\uDF00 - \uD86D\uDF34
    • \uD869[\uDF00-\uDFFF]
    • [\uD86A-\uD86C][\uDC00-\uDFFF]
    • \uD86D[\uDC00-\uDF34]
  • \uD86D\uDF40 - \uD86E\uDC1D
    • \uD86D[\uDF40-\uDFFF]
    • \uD86E[\uDC00-\uDC1D]
  • \uD87E\uDC00 - \uD87E\uDE1D
    • \uD87E[\uDC00-\uDE1D]

これらを整理して組み合わせると、最終的に以下のパターンが得られます。

/(?:[\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u3005\u3007\u3021-\u3029\u3038-\u303B\u3400-\u4DB5\u4E00-\u9FCC\uF900-\uFA6D\uFA70-\uFAD9]|[\uD840-\uD868][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|[\uD86A-\uD86C][\uDC00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D]|\uD87E[\uDC00-\uDE1D])/

というわけで「漢字 1 文字にマッチする JavaScript の正規表現パターン」でした。よいお年を。

IE で a 要素を使って相対 URL からスキームを得る2014年11月09日 20時22分

いくつかのブラウザではURLがhttpの相対リンクであってもelm.protocolが「:」や空文字になったりする

JavaScriptでリンク先URLがhttp/httpsか確認する方法 - 葉っぱ日記

この「いくつかのブラウザ」とは IE のことを指していると思われますが、href プロパティに絶対 URL を設定しなおすことで、URL のスキーム部分を取得できるようになります。

// http://www.example.org/ 上で実行しているものとする。
var elm = document.createElement('a');
elm.href = '/foo?bar#baz';

// IE 6 / 7 で絶対 URL を設定。
// elm.href = elm.cloneNode(false).href でもよい。
elm.href = elm.getAttribute('href', 4);

// IE (少なくとも 8 ~ 11) で絶対 URL を設定。
elm.href += '';

elm.protocol // => "http:"
elm.hostname // => "www.example.org"
elm.href     // => "http://www.example.org/foo?bar#baz"

しかし IE 6 / 7 の、getAttribute メソッドの第 2 引数に値 4 を渡すことで絶対 URL の値が返るという挙動は、いつ実装されいつ文書化されたのでしょうか?

modern.IE で配布されている IE 6 SP 3 検証環境では確かに動作するのですが、「IE の getAttribute / setAttribute」を書いた時点 (2005 年 10 月) では文書化されていなかった気がします。文書化されていたのなら、「IE 6,7 で相対URL -> 絶対 URL の変換 - #生存戦略 、それは - subtech」にまとめられたような cloneNode メソッドを使った手法を考えることもなかったでしょうから。

なお、「続・IEでのa要素の各属性について - 文殊堂」で言及されていますが、IE 6 / 7 では iframe 要素内の文書で上述の相対 URL 解決手法を用いると、(a 要素を文書木に挿入しない限り) 解決結果のホスト部分が親文書のものになってしまうようです。(ホスト部分が親文書のものになるという現象自体は IE 11 でも発生します。)

呼ばれていないけど、私もコードゴルフしてみました2013年08月09日 03時06分

Code 2013 というイベントで出されたというコードゴルフのお題「JavaScript でデジタル時計」を見ているうちに、自分でもやりたくなったので挑戦してみました。

基本方針

出力が複数行にわたるなら 1 行ずつ処理していくのが素直な手ですが、このお題においてはそれだとひとつの文字に対する処理が細切れになってしまいます。

1 行ずつ処理していく場合のイメージ図

そこで、ある文字の出力処理をまとめてやってしまいましょう。行ごとにではなく列ごとに処理を進めていくのです。

1 列ずつ処理していく場合のイメージ図

20 分

方針が決まれば早速コーディング。以前、渋谷から 10 分のゴルフ場で似たようなお題「banner」に取り組んだときは三十六進数表記を使いましたが、とりあえずは読み書きしやすいよう二進数表記で進めます。

本番の制限時間は 20 分だったそうなので、こちらも 20 分で到達したところはこちら、

t = 0;
setInterval(function () {
    s = ' ■';
    p = [
        '111111000111111',
        '111110000000000',
        '111011010110111',
        '111111010110101',
        '111110010011100',
        '101111010111101',
        '101111010111111',
        '111111000010000',
        '111111010111111',
        '111111010111101',
        '000000101000000',
    ].map(function (b) { return parseInt(b, 2) });
    l = ['', '', '', '', ''];
    d = [
        t / 60 / 10 | 0,
        t / 60 % 10 | 0,
        10,
        t % 60 / 10 | 0,
        t % 60 % 10 | 0,
    ];
    for (i in d) {
        v = p[d[i]];
        for (j = 20; j--;) {
            l[j % 5] += s[v & 1];
            v >>= 1;
        }
    }
    document.body.innerHTML = l.join('<br>');
    t++;
}, 1000);

スペース・改行を取り除いて 458 バイト (UTF-8 でエンコードした場合) と、ちょっと詰めきれていません。二進数表記文字列 + map メソッドの組み合わせを十進整数リテラルに書き換えるだけで 284 バイトになるので、あと一歩ではあったのですが。

改良

このお題ではひとつの文字を表すのに、3 列 5 行、15 個の点を使っています。ひとつの文字に対するパターンが 15 ビットで表現可能ということは、JavaScript の 1 文字 (16 ビット) に収まるわけですね。

ほかにも、変数 sp はそれぞれ 1 箇所でしか使っていないので、変数を使わずリテラルを直接書くといった節約の成果がこれです。

t = 0;
setInterval(function () {
    l = ["", "", "", "", ""];
    for (i in d = [t / 600, t / 60 % 10, 10, t % 60 / 10, t++ % 10])
        /* "\u7E3F\u7C00\u76B7\u7EB5\u7C9C\u5EBD\u5EBF\u7E10\u7EBF\u7EBD\u0140" */
        for (v = "縿簀皷纵粜庽庿縐线纽ŀ".charCodeAt(d[i]), j = 20; j--; v >>= 1)
            l[j % 5] += " ■"[v & 1];
    document.body.innerHTML = l.join("<br>")
}, 1e3)

スペース・改行を取り除いて 230 バイトまで減らせました。さらに、setInterval の第 1 引数を関数ではなく文字列にすることで、220 バイトとなります。

上述の版では文書読み込み完了から 1 秒間が空いて時計の描画が始まります。サンプルと同じく文書読み込み完了と同時に時計が描画される、修正版 (229 バイト) を作りました。v >>= 1v /= 2 にするという変更も入れています。