ぷろぐらみんぐ帳

C#とかJavaScriptとか

Tumblrのテキスト投稿でLightBoxを独自導入して使う

Tumblrにはbuilt-inのLightBoxがあり、カスタムHTML編集で簡単に導入することができる。以下のサイトで紹介されているのがこれを用いた方法。

f-u.seesaa.net

しかし、これはどうも写真投稿限定のようで、テキスト投稿になると画像のURLやサイズをbuilt-inのLightBoxに引き渡すことができなかった。似たような質問が英語記事だが紹介されている。

css-tricks.com

Unfortunately, if you’re using the built-in Tumblr lightbox for photosets there’s currently no way to apply it to single photos as well. What you can do is apply your own lightbox (eg. View.js) to single photos.

テキスト投稿はカスタム編集を見ていると、入力したHTMLソースをそのまま記事に渡しているようで、読み込んだあとにDOM操作してLightBoxのタグを付与しようとしても、画像のサイズや解像度を{block:Posts}の外側から{PhotoWidth-HighRes}の変数で取得することができず、built-inのLightBoxのポップアップが立ち上がらなかった。カスタムテーマ用の変数は以下を参照。

Tumblrカスタムテーマの作り方:日本語訳 (Creating a custom HTML theme | Tumblr)

{block:Posts}に入れ子になっている{block:Photo}や{block:Photos}も写真投稿用のブロックで、例えばブログのようにテキスト投稿で間に画像を挿入しながら書いていくという形式だと{block:Text}で読み込まれるようだ。{block:Text}において挿入されている画像をTumblrが用意した変数で取得することは不可能なので、テキスト投稿で入れた画像にLightBoxのような”味付け”をする場合は、結局のところDOM操作が必要になる。

結論からいうと、テキスト投稿で埋め込まれた画像においてLightBoxを導入することは可能だ。ただし、Tumblr側が用意したbuilt-inのLightBoxではなく、一からLightBoxを導入する必要がある

LightBoxの導入

まずはLightBoxの公式サイトにアクセスし、LightBoxをダウンロードする。

lokeshdhakar.com

英語になるが、公式のGetting Startedに目を通して使い方に慣れるといいだろう。

ダウンロードしたlightbox2-master.zipを解凍する。必要なファイルは、

  1. dist/css/lightbox.css

  2. dist/imagesのフォルダにある画像ファイル(close.png, loading.png, next.png, prev.png)の4つすべて

  3. dist/js/lightbox.js

この3種類だ。LightBoxjQueryを使用し、jQuery同梱版のLightBoxlightbox-plus-jquery.js)もあり公式チュートリアルではそれの使用を推奨する旨もあったが、自分の環境では同梱版を使うとTumblrjQueryを使用するプラグインと衝突するバグが発生したため、CDNからjQueryを読み込ませることにした。そのため使うのは同梱版ではない普通のlightbox.jsでよい。

Tumblrへのアップロード

ここが若干わかりづらいのだが、Tumblrにも静的ファイルをアップロードするスペースはある。

ダッシュボードから設定アイコン→テーマの編集を開き、「HTMLを編集」を開きカスタムHTML編集画面を起動する。そこからさらに左上の設定アイコン→「テーマアセット」を開く。ここが静的ファイルのアップロードスペースとなる。

テーマアセットの注意書きを見ると「テーマ以外のデータをアップロードした場合は、そのアカウントを随時凍結します」という物々しい注意書きがあるが、プラグイン用の画像やスクリプトファイルを上げるぐらいなら問題はないだろう(確証はないのでBANされても責任は取れません)。要はアップローダーみたいに使うのはNGということだろう。

まず、ここにimage以下の画像4ファイルをアップロードする。ここでのアップロード先のURLをコピーしてメモしておく。

次に、css/lightbox.cssCSSテキストエディタ等で編集する。なんでこんな面倒なことするのかというと、テーマアセットのアップロード先が不定かつ任意のディレクトリを作れないので、画像ファイルを相対パスから絶対パスに書き換えてやる必要があるからだ。LightBoxはMITライセンスなので改変についてのライセンス上の問題はない。

CSSの書き換え箇所は4箇所。例えばローディングの画像では、

.lb-cancel {
  display: block;
  width: 32px;
  height: 32px;
  margin: 0 auto;
  background: url(../images/loading.gif) no-repeat;
  //ここのurl以下を書き換える
}

url()のカッコ内をTumblrにアップロードした対応する画像ファイルのURLに書き換える。ファイル内を「url」で検索すると書き換え箇所が簡単に見つかる。

書き換えが終わったら、編集済みのlightbox.cssを同様にアップロードする。また、lightbox.jsもアップロードする。こちらは編集は特に必要ない。アップロードしたCSSとJSファイルのURLをまたメモしておく。

カスタムHTML編集

<head>~</head>にLightBoxCSS、<body>~</body>にjQueryLightBoxJavaScriptを設置する。

<head>
(中略)
<link rel="stylesheet" href="テーマアセットでアップロードしたURL/lightbox.css">
</head>

<body>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="テーマアセットでアップロードしたURL/lightbox.js"></script>
(中略)
</body>

使用しているテーマにあわせてDOMを操作

ここからは使用しているテーマによってクラス構造が違うので一概には言えないが、DOM編集のスクリプトを追加し、LightBoxでテキスト投稿の画像を表示できるようにする。

例えば自分の使用しているテーマでは、記事は次のような構造になっている(そのままソースを表示すると難読化されている可能性があるため、画像周辺を開発者ツールで見るとわかりすい)。

<div class="post-body">
  <p>町田は神奈川</p>
  <p>駅前走っているバスは神奈中~~~<img src="~~~.jpg" /></p>
</div>

これをLightBoxで表示するにはimgタグの部分を、

<a href="~~~.jpg" data-lightbox="ぶっちゃけなんでもいい"><img src="~~~.jpg" /></a>

とする。このdata-lightboxが重要でLightBoxで表示するという印である同時に、data-lightboxが同一の画像でグループ化して表示される

HTML編集画面を開き、bodyの最後あたりにDOM操作のスクリプトを追加する({block:Posts}のあとならどこでもよさそう)。

<script>
    var postImgs = document.getElementsByTagName("img");
for (var i = 0; i < postImgs.length; i++) {
    if (!postImgs[i].parentNode || 
        postImgs[i].parentNode.tagName.toLowerCase() !== "p") continue;
    if (!postImgs[i].parentNode.parentNode || 
        postImgs[i].parentNode.parentNode.className.toLowerCase() !== "post-body") continue;
    postImgs[i].outerHTML = "<a href='" + postImgs[i].src + 
        "' data-lightbox='lightbox" + i + "'><img src='" + postImgs[i].src + 
        "' alt='" + postImgs[i].alt + "' /></a>";
}
</script>

これでOKだ。プレビュー画面では反映されないことがあるので、保存してから別ページでホームを開くとLightBoxで画像が表示されるようになっている。せっかくjQuery導入したからここをjQueryで書いてもよかったけど個人的に嫌いなので(

以前投稿した記事をLightBox用に再編集する必要はない。

OpenCvSharp3で画像合成

メモがてらに。OpenCV独特のMat形式の扱いに慣れるのが大変だった。

hotspring.jpgはWikipediaより。これとレナをアルファブレンディングで合成してみる。 f:id:enjyuu:20171123210436p:plain f:id:enjyuu:20171125150140j:plain

var bigImg = new Mat("hotspring.jpg");

using (var smallImg = new Mat("lenna.png"))
{
    //貼り付ける画像を50%縮小
    var contract = new Mat();
    Cv2.Resize(smallImg, contract, new OpenCvSharp.Size(0.5 * smallImg.Width, 0.5 * smallImg.Height), 0, 0, InterpolationFlags.Lanczos4);

    //貼り付ける範囲を指定して貼り付け
    var rect = new Rect((bigImg.Width - contract.Width) / 2, 0, contract.Width, contract.Height);
    var pastedMat = new Mat();
    Cv2.AddWeighted(new Mat(bigImg, rect), 0.2, contract, 0.8, 0, pastedMat);
    //※AddWeightedを使わずにAddを使うとアルファブレンディングしない<s>ただの貼り付け</s>

    //貼り付けた部分を置き換え
    bigImg[rect] = pastedMat;

    //保存
    Cv2.ImWrite("lenna_hotspring.png", bigImg);

    contract.Dispose();
    pastedMat.Dispose();
    bigImg.Dispose();
}

ちなみにAddWeightedを使わずにAddを使うとただの貼り付け合成になる→訂正:加算になったので、ただの貼り付けをしたい場合はbigImg[rect]=smallImgのように素直に置き換えましょう。サイズが異なる画像を貼り付けるときは貼り付ける範囲のMatを新しく作るのがポイント。

f:id:enjyuu:20171125150151p:plain

C#でLanczos法による画像リサイズ

画像をリサイズ+αをするプログラムを作りたくなったので、Lanczos法の実装を行ってみた。

自力で実装

クォリティはさておき、とりあえず一回ぐらい自力で実装してみようということでまずは考え方から。このサイトが参考になった。

http://cafe.mis.ous.ac.jp/2015/sawasemi/画像補間法による拡大.pdf

このサイトでは拡大のみだが、拡大にしろ縮小にしろ要するにピクセル単位で整数の離散量である画素値を、連続的に取り出すのにはどうすればいいか、という問題であると勝手に脳内変換。

もう少し具体的に書くと、例えば100x1の画像を40x1に縮小する場合を考える(この場合は実質的には1次元になるので縦方向は無視)。元画像の座標をsrc(x)、縮小画像の座標をdest(x)、0≦src(x)≦99、0≦dest(x)≦39。dest(x)を基準にして対応するsrc(x)を求めると、

dest(0)=src(0), dest(1)=src(2.5), dest(2)=src(5), ……, dest(39)=src(97.5) 

元画像の2.5ピクセルや97.5ピクセルって取り出せないじゃん?どうやって取り出すの?→補間法を使う→Lanczos法はその補間法の1つと解釈するとわかりやすそう。上のサイトではニアレストネイバー法ですら、バイリニア法やLanczos法と同様の加重関数の表記に帰着させることが示されていてなるほどと思った。

突っ込みどころありありなソースだが、自分で実装してみたものがこちら。

public class LanczosCanvas : IDisposable
{
    private Bitmap srcCanvas { get; set; }

    public LanczosCanvas(Bitmap src)
    {
        srcCanvas = src;
    }

    public void Dispose()
    {
        srcCanvas.Dispose();
    }

    //重み関数
    private double WeightFunction(double distanceX, double distanceY, LanczosMethod method)
    {
        double n = method.GetDimension();
        if (n == 0.0) return 0.0;

        if (Math.Abs(distanceX) >= n || Math.Abs(distanceY) >= n) return 0.0;
        else return Sinc(distanceX) * Sinc(distanceX / n) *
                Sinc(distanceY) * Sinc(distanceY / n);
    }

    private double Sinc(double x)
    {
        if (x == 0.0) return 1.0;//これをやらないとNaNになる
        else return Math.Sin(Math.PI * x) / Math.PI / x;

    }

    //ピクセル取得
    private Color GetSrcPixel(double x, double y)
    {
        //鏡面法
        var mx = Mirror(x, srcCanvas.Width);
        var my = Mirror(y, srcCanvas.Height);
        if (mx < 0 || my < 0) throw new ArgumentOutOfRangeException("元画像の画素数が足りません");
        return srcCanvas.GetPixel(mx, my);
    }

    private int Mirror(double s, int limit)
    {
        if (s < -limit + 1) return -1;
        else if (s < 0) return (int)Math.Round(-s, 0);
        else if (s <= limit - 1) return (int)Math.Round(s, 0);
        else if (s <= 2 * limit - 2) return (int)Math.Round(2 * limit - s - 2, 0);
        else return -1;
    }

    //リサイズ
    public Bitmap Resize(int destWidth, int destHeight, LanczosMethod method)
    {
        var destCanvas = new Bitmap(destWidth, destHeight);

        var n = (int)method.GetDimension();
        var diffs = Enumerable.Range(- n + 1, 2 * n - 1).ToList();

        foreach(var i in Enumerable.Range(0, destWidth))
        {
            foreach(var j in Enumerable.Range(0, destHeight))
            {
                //ソースキャンバスでの相対座標
                var rx = (double)i / (double)destWidth * (double)srcCanvas.Width;
                var ry = (double)j / (double)destHeight * (double)srcCanvas.Height;

                //ソースから取得する座標
                var points = new List<Tuple<double, double, double, double>>();
                foreach(var p in diffs)
                {
                    var dx = p + Math.Floor(rx) - rx;
                    if (dx > n) continue;
                    foreach (var q in diffs)
                    {
                        var dy = q + Math.Floor(ry) - ry;
                        if (dy > n) continue;

                        var point = new Tuple<double, double, double, double>
                            (p + Math.Floor(rx), q + Math.Floor(ry), dx, dy);
                        points.Add(point);
                    }
                }

                //補間計算
                double desta = 0.0, destr = 0.0, destg = 0.0, destb = 0.0;
                foreach(var p in points)
                {
                    var src = GetSrcPixel(p.Item1, p.Item2);
                    var weight = WeightFunction(p.Item3, p.Item4, method);

                    desta += weight * src.A;
                    destr += weight * src.R;
                    destg += weight * src.G;
                    destb += weight * src.B;
                }

                desta = Math.Min(Math.Max(0, desta), 255);
                destr = Math.Min(Math.Max(0, destr), 255);
                destg = Math.Min(Math.Max(0, destg), 255);
                destb = Math.Min(Math.Max(0, destb), 255);

                //リサイズ先にセット
                destCanvas.SetPixel(i, j, Color.FromArgb((int)desta, (int)destr, (int)destg, (int)destb));
            }
        }

        return destCanvas;
    }
}

public enum LanczosMethod
{
    Lanczos2, Lanczos3, Lanczos4,
}

public static class Extensions
{
    public static double GetDimension(this LanczosMethod method)
    {
        switch(method)
        {
            case LanczosMethod.Lanczos2: return 2.0;
            case LanczosMethod.Lanczos3: return 3.0;
            case LanczosMethod.Lanczos4: return 4.0;
            default: return 0.0;
        }
    }
}

はっきり言って遅い。Lanczos4で1200x900の画像を縮小しようとすると20秒近くかかる。なので、これを実践的に使おうと思ったら、Sinc関数の値をキャッシュしたり(どうせ似たようなxしか放り込まれないから)、GetPixel()が重ければBitMapじゃなくてARGB値をキャッシュしたりなど工夫が必要(アンマネージ使わなくても呼び出し回数を減らすだけでそこそこ早くなるはず)。

結果はこちら。おなじみのレナさんをLanczos4で40%縮小してみた。

元画像(Wikipediaより) f:id:enjyuu:20171123210436p:plain

40%縮小 f:id:enjyuu:20171123210446p:plain

拡大してみると、帽子や背景に古い写真のような謎のざらつきが見える。(左:元画像、右:縮小画像) f:id:enjyuu:20171123210924p:plain

この原因はよくわからないが、後述のOpenCVを使った方法と比べるとこれ固有の現象であるようなので、この実装に問題があるのかと思われる。

OpenCVSharpを使った方法

よりスマートな方法として、リサイズのアルゴリズムOpenCVにまかせてしまう。C#のラッパーとしてOpenCvSharpがある(現行はOpenCvSharp3)。NuGetからインストールできる。

github.com

使い方としては、NuGetからOpenCvSharp3をインストールし、lenna.pngデバッグフォルダにコピーした上で、

//using OpenCvSharp;

using (var src = new Mat("lenna.png"))
{
    using (var dst = new Mat())
    {
        Cv2.Resize(src, dst, new OpenCvSharp.Size(205, 205), 0, 0, InterpolationFlags.Lanczos4);
        Cv2.ImWrite("lenna_opencv_small.png", dst);
    }
}

たったこれで終わり。処理時間もせいぜい数秒で、Lanczos4を使っても自前実装よりも断然早い。OpenCVで40%縮小したものがこれで、

f:id:enjyuu:20171123212028p:plain

帽子の部分を拡大して比較すると(左:自前実装による縮小、右:OpenCVによる縮小) f:id:enjyuu:20171123212541p:plain

右(OpenCV)のほうがざらつきが少なくなっている。

ライセンス等の問題がなく個人的に利用する場合、画像のリサイズはOpenCVにまかせてしまうのが良さそうだ

OpenCvSharp3の入門はこちらが詳しい。

sourcechord.hatenablog.com

VisualStudioで生のReact.js+linq.js

前々からやってみたかったReact.jsを触ってみた。詳しいことはまだちゃんと把握していないぺーぺーなのだが、思想が.NETのWPFやRxそのものなので親近感があったので。.NETっぽくしたかったのでlinq.jsも使ってみた。結論から言うと意外と相性が良かった。

プロジェクト作成

VisualStudio2015の例。せっかくなのでReactもlinqもNuGet経由でインストールする。

  1. ファイル→新規作成→プロジェクト
  2. 新しいプロジェクトが出てきたら、テンプレート→Visual C#→Web→「ASP.NET Web Application」
  3. New ASP.NET Projectが出たら、とりあえず「Empty」を選択
  4. Configure Microsoft Azure Web Appが出てくるが、関係ないのでキャンセルを選択
  5. プロジェクトができたら、プロジェクトを右クリック→追加→新しい項目
  6. 新しい項目の追加から、Visual C#→Web→「HTMLページ」を選択、名前を「main.html」とする
  7. main.htmlを右クリック→スタートページに設定
  8. プロジェクトを右クリック→NuGetパッケージの管理
  9. 参照から、「React.js」と「linq.js」を検索し、インストール
  10. React.jsは「Scripts/react」以下に、linq.jsは「Scripts」以下にインストールされている。

React.jsで静的なWebページを作る

簡単な例として表を作ってみる。ボタン操作等でDOMを書き換えるのはまた今度で。JSX記法はBabelをインストールしないといけないので、JSXなしで作ってみた(ちょっと面倒だけど意外といける)。このページが大変参考になった。 postd.cc まずHTML部分から。

<body>
    <script type="text/javascript" src="Scripts/react/react.js"></script>
    <script type="text/javascript" src="Scripts/react/react-dom.js"></script>
    <script type="text/javascript" src="Scripts/linq.js"></script>
    <div id="root"><!--ルートコンポーネント--></div>

    <script type="text/javascript">
    /* ここにReactのコードが入る */
    </script>
</body>

最初は、React.renderが非推奨になりReactDOM.renderが推奨になったので、react.jsだけでなくreact-dom.jsも外部ファイルから呼び出している。他にlinq.jsも呼び出した。以下。Reactのコード部分について。

まずは表示したいデータ。東海地方の4県の人口や県庁所在地、政令指定都市JSON形式で与えた。

var prefs = [
    { key: 1, name: "静岡県", prefectoralCapital: "静岡市", designatedCities: ["静岡市", "浜松市"], population: 369 },
    { key: 2, name: "愛知県", prefectoralCapital: "名古屋市", designatedCities: ["名古屋市"], population: 751 },
    { key: 3, name: "岐阜県", prefectoralCapital: "岐阜市", population: 202 },
    { key: 4, name: "三重県", prefectoralCapital: "津市", population: 181 }
];

これをtableとして表示する。行単位のクラスを定義する。

var Prefecture = React.createClass({
    propTypes: {
        name: React.PropTypes.string.isRequired,
        prefectoralCapital: React.PropTypes.string.isRequired,
        designatedCities: React.PropTypes.array,
        population: React.PropTypes.number.isRequired,
    },
    render: function () {
        var designated = "";
        if (this.props.designatedCities) designated = this.props.designatedCities.join(",");

        return React.createElement("tr", { key: this.props.name },
            React.createElement("th", { key: this.props.name + "_1" }, this.props.name),
            React.createElement("td", { key: this.props.name + "_2" }, this.props.prefectoralCapital),
            React.createElement("td", { key: this.props.name + "_3" }, designated),
            React.createElement("td", { key: this.props.name + "_4" }, this.props.population)
            );
    }
})

Reactの面倒なところに、各要素ごとにkeyを与えないといけない(これが結構ハマった)。tr要素だけにkeyを与えても、コンソールのエラーに

0x800a139e - JavaScript 実行時エラー: Warning: Each child in an array or iterator should have a unique “key” prop. Check the top-level render call using . See https://fb.me/react-warning-keys for more information.

と表示される。えっtrにkey与えたやん!とやっても一向にエラーが消えない。Stackoverflowを見たら似た現象があって、 stackoverflow.comtrだけじゃなくてtdやthにもkeyを与えろ」とのこと。なにそれめんどくさい。解決法にもあったが、Reactで表を作るときはtableで作るのではなく、CSSでdiv要素を使って表みたいなスタイルを定義してやるのが楽かもしれない。確かに、thやthにもkeyを与えるとエラーは消える。

表のコンポーネントを作っていく。

var tableElements = Enumerable.From(prefs).Select(function (p) { return React.createElement(Prefecture, p) }).ToArray();
var tableHeader = React.createElement("tr", { key: "header" },
    Enumerable.From(["県名", "県庁所在地", "政令指定都市", "人口(万人)"]).Select(function (header) {
        return React.createElement("th", { key: header }, header);
    }).ToArray());
tableElements.unshift(tableHeader);

普通はmap関数を使うが、linqでも普通にできる(だってこっちのほうが読みやすいし)。Reactのエレメントは子のエレメントを持っているという構造上、多分linqと相当相性いいんじゃないかと思う。最後にルートエレメントを作ってReactDOMにぶちこむ。

var rootElement =
    React.createElement("table", { key: "table" },
      React.createElement("tbody", { key: "tbody" }, tableElements)
    );

ReactDOM.render(rootElement, document.getElementById("root"));

結果はこちら。至って普通のtableである。
f:id:enjyuu:20170819050510p:plain
これだけといったらこれだけだが、Reactの記事あんまりないのでこんなへっぽこなものでも参考になれば幸い。もうちょっとひねって動的な操作をしてReactの本領発揮させたいところ。

JavaScriptの日付計算を(少し)楽にする

JavaScriptで日付計算やったら発狂しそうになったんで、多少楽にする方法を考えた。

C#みたいにAddHours()で時間計算する関数を追加してみた(C#の日付計算も結構地雷あってハマるからあんまり両手を上げて褒められないのがアレ)。

Dateの拡張

Date.prototype.addYear = function (yearValue) {
    var result = new Date(this.getTime());
    result.setFullYear(this.getFullYear() + yearValue);
    return result;
}
Date.prototype.addMonth = function (monthValue) {
    var result = new Date(this.getTime());
    result.setMonth(this.getMonth() + monthValue);
    return result;
}
Date.prototype.addDate = function (dateValue) {
    var result = new Date(this.getTime());
    result.setDate(this.getDate() + dateValue);
    return result;
}
Date.prototype.addHours = function (hoursValue) {
    var result = new Date(this.getTime());
    result.setHours(this.getHours() + hoursValue);
    return result;
}
Date.prototype.addMinutes = function (minutesValue) {
    var result = new Date(this.getTime());
    result.setMinutes(this.getMinutes() + minutesValue);
    return result;
}
Date.prototype.addSeconds = function (secondsValue) {
    var result = new Date(this.getTime());
    result.setSeconds(this.getSeconds() + secondsValue);
    return result;
}

Dateのprototypeをこのように拡張してラップ。返り値もDateにしてメソッドチェーンできたほうが使いやすそうなのでそうしてみた(あくまで主観)。

もともとのJavaScriptのではsetHoursのようにもともとの変数の値を変えていくが、拡張したadd○○では新しく変数を作っているため、計算結果が計算前の変数に影響を与えない。以下サンプル。

var now = new Date();
document.writeln(now.toString());//Sat Jul 22 2017 23:05:39 GMT+0900 (東京 (標準時))
document.writeln(now.addYear(-1));//Fri Jul 22 2016 23:05:39 GMT+0900 (東京 (標準時))
document.writeln(now.addMonth(13));//Wed Aug 22 2018 23:05:39 GMT+0900 (東京 (標準時))
document.writeln(now.addDate(-5));//Mon Jul 17 2017 23:05:39 GMT+0900 (東京 (標準時))
document.writeln(now.addYear(1).addMonth(-2).addDate(3));
  //Fri May 25 2018 23:05:39 GMT+0900 (東京 (標準時)) メソッドチェーンもできる

document.writeln(now.addHours(72));//Tue Jul 25 2017 23:05:39 GMT+0900 (東京 (標準時))
document.writeln(now.addHours(0.5));
  //Sat Jul 22 2017 23:05:39 GMT+0900 (東京 (標準時)) 残念ながら小数点以下は反映されない
document.writeln(now.addMinutes(-45));//Sat Jul 22 2017 22:20:39 GMT+0900 (東京 (標準時))
document.writeln(now.addSeconds(114514));//Mon Jul 24 2017 06:54:13 GMT+0900 (東京 (標準時))
document.writeln(now.addHours(114).addMinutes(514).addSeconds(1919));
  //Fri Jul 28 2017 02:11:38 GMT+0900 (東京 (標準時))

引数の小数点以下のaddが無視されるのが少し残念かもしれない。getTime()等でミリ秒単位で計算させれば小数以下も反映されるはず。

NaNのみ除外する数値判定

JavaScriptの闇が深い真偽値の話。NaNを含む数値列に対して一括計算するときにどうする?という問題。

isNaN()の場合

NaNのみ判定したければisNaN()という関数がある。ECMAScript 1stから対応。とりあえず評価用に次のようなtest関数を作ってみる。 developer.mozilla.org

function test(func) {
    function testunit(val) {
        return ('' + func).replace(/^\s*function\s*([^\(]*)[\S\s]+$/im, '$1') + "(" + val + ") is " + func(val) + "<br/>";
    }
    var str = "";
    str += testunit(true);
    str += testunit(false);
    str += testunit(null);
    str += testunit(undefined);
    str += testunit(0);
    str += testunit(1);
    str += testunit(-3.1);
    str += testunit(-3.0);
    str += testunit(012);//8進数で10
    str += testunit(0xA);//16進数で10
    str += testunit(Math.PI);
    str += testunit(Infinity);
    str += testunit(NaN);
    str += testunit("");
    str += testunit(" ");
    str += testunit("abc");
    str += testunit("123");
    str += testunit(console);

    document.write(str);
}

デバッグ環境はIE11。Function.nameプロパティが使えないため、関数名の取得は正規表現で取っている。test(isNaN)を実行すると、

//isNaNの実行結果
isNaN(true) is false
isNaN(false) is false
isNaN(null) is false
isNaN(undefined) is true
isNaN(0) is false
isNaN(1) is false
isNaN(-3.1) is false
isNaN(-3) is false
isNaN(10) is false
isNaN(10) is false
isNaN(3.141592653589793) is false
isNaN(Infinity) is false
isNaN(NaN) is true
isNaN() is false
isNaN( ) is false
isNaN(abc) is true
isNaN(123) is false
isNaN([object Console]) is true

NaNのほかundefinedや空白以外の文字列、オブジェクトもtrueとなっているのが注意。この理由は書いてあった。

true を返す場合、x を使用すると全ての算術式で NaN を返すことになります。これはつまり、JavaScriptにおいて isNaN(x) == true という式は、x - 0 という式が NaN を返すかどうか、というケースと同等である(JavaScript では x - 0 == NaN は常に false を返すため、このことを確認できませんが)ということです。

なるほど、算術式で計算したときにNaNとなるのがisNaN()であると。

ちなみに、Number.isNaN()という関数もあり、型変換の問題の影響がないらしい。ただし、Number.isNaNはECMAScript 2015以降なので、IEは非対応Google Chromeが対応しているのでそれでデバッグをする。test(Number.isNaN)の実行結果

//Number.isNaN()の場合(IE非対応)
isNaN(true) is false
isNaN(false) is false
isNaN(null) is false
isNaN(undefined) is false
isNaN(0) is false
isNaN(1) is false
isNaN(-3.1) is false
isNaN(-3) is false
isNaN(10) is false
isNaN(10) is false
isNaN(3.141592653589793) is false
isNaN(Infinity) is false
isNaN(NaN) is true
isNaN() is false
isNaN( ) is false
isNaN(abc) is false
isNaN(123) is false
isNaN([object Object]) is false

isNaN()とNumber.isNaN()の結果が違うだと…。感覚的にはNumber.isNaN()のほうがマッチしやすくて、こちらは本当にNaNだけtrueを返す

isFinite()の場合

似たような関数に、有限数かどうかを判定するisFinite()がある。ECMAScript 3rdから。 developer.mozilla.org test(isFinite)を実行すると、

//isFinite()の場合
isFinite(true) is true
isFinite(false) is true
isFinite(null) is true
isFinite(undefined) is false
isFinite(0) is true
isFinite(1) is true
isFinite(-3.1) is true
isFinite(-3) is true
isFinite(10) is true
isFinite(10) is true
isFinite(3.141592653589793) is true
isFinite(Infinity) is false
isFinite(NaN) is false
isFinite() is true
isFinite( ) is true
isFinite(abc) is false
isFinite(123) is true
isFinite([object Console]) is false

trueやfalse、null、空白文字列や"123"のような文字列の数値がtrueとなっている。nullではtrueなのに、undefinedだとfalseなのが非常にいやらしい。Mozillaのページを読んでいくと「より堅牢性の高いNumber.isFiniteでは~」という記述がある。Number.isNaNと同様に、Number.isFiniteはECMAScript 2015以降なので、IEは非対応Google Chromeでのtest(Number.isFinite)の実行結果

//Number.isFinite()の場合(IE非対応)
isFinite(true) is false
isFinite(false) is false
isFinite(null) is false
isFinite(undefined) is false
isFinite(0) is true
isFinite(1) is true
isFinite(-3.1) is true
isFinite(-3) is true
isFinite(10) is true
isFinite(10) is true
isFinite(3.141592653589793) is true
isFinite(Infinity) is false
isFinite(NaN) is false
isFinite() is false
isFinite( ) is false
isFinite(abc) is false
isFinite(123) is false
isFinite([object Object]) is false

やはりisFinite()とNumber.isFinite()の結果が違う。Number.isFinite()のほうが感覚的にマッチしやすい。IEのことをばっさり切り捨ててしまうなら、数値判定はNumber.isFinite()で十分だろう。もしInfinityを入れたいのなら、条件でプラスしてやればよさそうだ(もう少しいいやり方あるかもしれない)。

typeofを使う

数値判定一般なら、Number.isFinite()を使えばいいことがわかったが、悲しいことにIE非対応なのでどうにかする方法を考える。先程のNumber.isNaNのMozillaのページを見ると「ポリフィル」のところに面白いコードがあった。

Number.isNaN = Number.isNaN || function(value) {
    return typeof value === "number" && value !== value;
}

これ初めてみたときは目からうろこで、なるほどtypeofを使うのか!確かにtypeofならECMAScript 1stからなので、IEでも十分いけそう。「value !== value」でなぜNaNが除外できるかというと、 developer.mozilla.org

NaN は別の NaN 値を含むあらゆる数と同じではないと比較されます(==、!=、===、!== によって)。ある値が NaN かどうかを的確に判定するには Number.isNaN() か isNaN() を使用してください。あるいは自己比較を実行しましょう。NaNだけが、自身と同等ではないと比較評価されます

ということは、IEでもこんな関数を定義してやればNaNは弾けることになる。

function isValidNumber(val) {
    return typeof val === "number" && val === val;
}

test(isValidNumber);
isValidNumber(true) is false
isValidNumber(false) is false
isValidNumber(null) is false
isValidNumber(undefined) is false
isValidNumber(0) is true
isValidNumber(1) is true
isValidNumber(-3.1) is true
isValidNumber(-3) is true
isValidNumber(10) is true
isValidNumber(10) is true
isValidNumber(3.141592653589793) is true
isValidNumber(Infinity) is true
isValidNumber(NaN) is false
isValidNumber() is false
isValidNumber( ) is false
isValidNumber(abc) is false
isValidNumber(123) is false
isValidNumber([object Console]) is false

ようやく望む結果が出てきた。ちなみに&&以降を!isNaN(val)で置き換えても同じ結果になる。処理時間計測してみたらほとんど変わらなかったんでお好みで。Infinityを除外したければ&&以降をisFinite(val)でやればOK。

linq.jsで使う

なんでこんなことを長々と調べてたかというと、linq.jsでNaNが入っているデータに対して平均を求める問題を考えたかったため。このライブラリは計算途中をトレースする機能があるが、LINQ自体計算がブラックボックス化しがちなので、JavaScriptのように独特の型仕様が入ると思わぬバグでハマりそうだった。

やっちゃダメな例だけど、if(x)で変な数値除外できればいいやとか適当なことやってると、平均でやばいことになる。(この例の正しい平均は1.5)

<script type="text/javascript" src="Scripts/linq.js"></script>
<script type="text/javascript">
    var array = [1, 2, 3, NaN, 0];
    var sum = Enumerable.From(array).Where("$").Sum();
    document.write("sum = " + sum + "<br/>");
    var average = Enumerable.From(array).Where("$").Average();
    document.write("average = " + average + "<br/>");
</script>
//sum = 6 ← 合計は何も問題ない
//average = 2 ← 平均で0が除外されてしまっている!!

簡単な例だからすぐ気づくが、データ数が多くなったり複雑な計算させると多分気づかないと思う(自分はこれバグだって気付ける自信ないです…)。

なのでこうしよう。

<script type="text/javascript">
    function isValidNumber(val) {
        return typeof val === "number" && val === val;
    }

    var array = [1, 2, 3, NaN, 0];
    var sum = Enumerable.From(array).Where("isValidNumber($)").Sum();
    document.write("sum = " + sum + "<br/>");
    var average = Enumerable.From(array).Where("isValidNumber($)").Average();
    document.write("average = " + average + "<br/>");
</script>
//sum = 6
//average = 1.5←正しい値に

JavaScriptの動的型付けは飛び道具的なことできて面白いこともあるが、すごく独特の挙動を示すので気をつけないとほんとしねる。

linq.jsをVisualStudioで使う

C#に慣れているとJavaScriptでもLINQを使いたくなることがある。linq.js自体の使い方はいろいろ探すと出て来るが、JavaScriptIDEとしてVisualStudioを使うことができるのでVS内で完結させてみた。IntelliSenseのおかげでほとんどC#と同じような感覚で扱える。

以下、VisualStudio2015での例。linq.jsはNuGetから取れる

  1. 開始→新しいプロジェクトから、テンプレート→Visual C#→Web→「ASP.NET Web Application」を選択。(例として、ソリューション名はWebApplication1とする)
  2. New ASP.NET Projectの設定が出てきたらとりあえず「Empty」を選択 f:id:enjyuu:20170709160424p:plain
  3. 「Configure Microsoft Azure Web App」が画面が出てきたらとりあえずキャンセル
  4. ソリューションができたら、ソリューションエクスプローラーから、プロジェクト「WebApplication1」を右クリック→追加→新しい項目→「VisualC#→Web→HTMLページ」(名前はmain.htmlとする) f:id:enjyuu:20170709160428p:plain
  5. ソリューションエクスプローラーの「main.html」を右クリック→スタートページに設定
  6. プロジェクト「WebApplication1」を右クリック→NuGetパッケージの管理→参照→「linq.js」で検索しインストー(いろいろ確認ダイアログ出てくるがOKで) f:id:enjyuu:20170709160432p:plain
  7. インストールが終わるとこのようにScriptsのフォルダができている。あとは煮るなり焼くなり f:id:enjyuu:20170709160736p:plain

導入

main.htmlのの中を次のようにする。以下、サンプルデータとしてprefsを使う。

    <script type="text/javascript" src="Scripts/linq.js"></script>
    <pre>
    <script type="text/javascript">
        var prefs = [
          {
              "index": 15, "name": "新潟県", "prefecturalCapital": "新潟市",
              "highestPeak": "小蓮華山", "highestPoint": 2766, "designatedCities": ["新潟市"]
          },
          {
              "index": 16, "name": "富山県", "prefecturalCapital": "富山市",
              "highestPeak": "立山", "highestPoint": 3015
          },
          {
              "index": 17, "name": "石川県", "prefecturalCapital": "金沢市",
              "highestPeak": "白山", "highestPoint": 2702
          },
          {
              "index": 18, "name": "福井県", "prefecturalCapital": "福井市",
              "highestPeak": "越前三ノ峰", "highestPoint": 2095
          },
          {
              "index": 19, "name": "山梨県", "prefecturalCapital": "甲府市",
              "highestPeak": "富士山", "highestPoint": 3776
          },
          {
              "index": 20, "name": "長野県", "prefecturalCapital": "長野市",
              "highestPeak": "奥穂高岳", "highestPoint": 3190
          },
          {
              "index": 21, "name": "岐阜県", "prefecturalCapital": "岐阜市",
              "highestPeak": "奥穂高岳", "highestPoint": 3190
          },
          {
              "index": 22, "name": "静岡県", "prefecturalCapital": "静岡市",
              "highestPeak": "富士山", "highestPoint": 3776, "designatedCities": ["静岡市", "浜松市"]
          },
          {
              "index": 23, "name": "愛知県", "prefecturalCapital": "名古屋市",
              "highestPeak": "茶臼山", "highestPoint": 1415, "designatedCities": ["名古屋市"]
          }
        ];
    </script></pre>

具体的なコードはprefs以下に。

linq.jsのどこがいいの?

→明らかに書く量が減る。例えばindexが偶数のデータだけ取り出してJSON文字列に直せとすると、linq.jsを使わない場合、

        var evens = [];
        for (var i = 0; i < prefs.length; i++) {
            if (prefs[i].index % 2 == 0) evens.push(prefs[i]);
        }
        document.writeln(JSON.stringify(evens));

これがlinq.jsの場合こうじゃ↓

        document.writeln(Enumerable.From(prefs).Where("$.index%2==0").ToJSON());

これで終わり。余計なループを書かなくてよくなってコードの可読性が上がるのもポイント(少なくとも自分は下のほうが見やすい)。

サンプルと実行結果

        //県名列挙
        Enumerable.From(prefs).WriteLine("$.name");
        /* -実行結果-
        新潟県
        富山県
        石川県
        福井県
        山梨県
        長野県
        岐阜県
        静岡県
        愛知県*/
        document.writeln();

        //indexが偶数番目の県+JSON化
        document.writeln(Enumerable.From(prefs).Where("$.index%2==0").ToJSON());
        /* -実行結果-
        [{"index":16,"name":"富山県","prefecturalCapital":"富山市","highestPeak":"立山","highestPoint":3015},{"index":18,"name":"福井県","prefecturalCapital":"福井市","highestPeak":"越前三ノ峰","highestPoint":2095},{"index":20,"name":"長野県","prefecturalCapital":"長野市","highestPeak":"奥穂高岳","highestPoint":3190},{"index":22,"name":"静岡県","prefecturalCapital":"静岡市","highestPeak":"富士山","highestPoint":3776,"designatedCities":["静岡市","浜松市"]}]*/
        document.writeln();

        //○○を最高峰とする県で最初にヒットしたもの
        var fuji = Enumerable.From(prefs).Where("$.highestPeak=='富士山'").First();
        document.writeln(JSON.stringify(fuji));
        /* -実行結果-
        {"index":19,"name":"山梨県","prefecturalCapital":"甲府市","highestPeak":"富士山","highestPoint":3776}*/
        fuji = Enumerable.From(prefs).Where("$.highestPeak=='富士山'").FirstOrDefault();
        document.writeln(JSON.stringify(fuji));
        /* 実行結果は同じ*/
        //var kita = Enumerable.From(prefs).Where("$.highestPeak=='北岳'").First();//ヒットしない→これは例外になる
        var kita = Enumerable.From(prefs).Where("$.highestPeak=='北岳'").FirstOrDefault();//ヒットしないが例外にはならない(undifinedになる)
        document.writeln(kita);
        /* -実行結果-
        undefined */
        document.writeln();

        //LINQ+ループ
        fuji = Enumerable.From(prefs).Where("$.highestPeak=='富士山'").ToArray();//ToArrayをしないとEnumerableのメソッドが渡される
        for (var f in fuji) {
            document.writeln(fuji[f].name);
        }
        /* -実行結果-
        山梨県
        静岡県*/
        document.writeln();

        //Dictionary化 (linq.jsで.NETのDictionaryを模して作られたコレクションなので、JavaScriptの連想配列とは異なる)
        Enumerable.From(prefs).ToDictionary("$.index", "$.name").ToEnumerable().WriteLine("'key:'+$.Key+' value:'+$.Value");
        /* -実行結果-
        key:15 value:新潟県
        key:16 value:富山県
        key:17 value:石川県
        key:18 value:福井県
        key:19 value:山梨県
        key:20 value:長野県
        key:21 value:岐阜県
        key:22 value:静岡県
        key:23 value:愛知県*/
        document.writeln();

        //政令指定都市がある県とその都市の一覧
        Enumerable.From(prefs).Where("$.designatedCities").SelectMany("$.designatedCities", "f,m => f.name+m").WriteLine();
        /* -実行結果-
        新潟県新潟市
        静岡県静岡市
        静岡県浜松市
        愛知県名古屋市 */
        document.writeln();

        //最高標高で降順ソート
        Enumerable.From(prefs).OrderByDescending("$.highestPoint")
            .Select("$.index+' '+$.name+' '+$.highestPeak+' '+$.highestPoint").WriteLine();
        /* -実行結果-
        22 静岡県 富士山 3776
        19 山梨県 富士山 3776
        21 岐阜県 奥穂高岳 3190
        20 長野県 奥穂高岳 3190
        16 富山県 立山 3015
        15 新潟県 小蓮華山 2766
        17 石川県 白山 2702
        18 福井県 越前三ノ峰 2095
        23 愛知県 茶臼山 1415*/
        document.writeln();

        //最高標高で降順、IDで昇順、最初の4つを取る
        Enumerable.From(prefs).OrderByDescending("$.highestPoint").ThenBy("$.index").Take(4)
            .Select("$.index+' '+$.name+' '+$.highestPeak+' '+$.highestPoint").WriteLine();
        /* -実行結果-
        19 山梨県 富士山 3776
        22 静岡県 富士山 3776
        20 長野県 奥穂高岳 3190
        21 岐阜県 奥穂高岳 3190*/
        document.writeln();

        //最高標高で昇順、IDで昇順、3つを取る(関数表記もできる:入力補間が働くので好みで)
        Enumerable.From(prefs)
        .OrderBy(function (x) { return x.highestPoint })
        .ThenBy(function (x) { return x.index })
        .Take(3).Select("$.index+' '+$.name+' '+$.highestPeak+' '+$.highestPoint").WriteLine();
        /* -実行結果-
        23 愛知県 茶臼山 1415
        18 福井県 越前三ノ峰 2095
        17 石川県 白山 2702*/

特に政令指定都市の列挙や、ソートの条件が複数の例は、linq.jsを使わないとかなり面倒なことになると思う。さあみんなもLINQを崇めよう。 linq.js Reference