ぷろぐらみんぐ帳

C#とかJavaScriptとか

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

連想配列のJSONをパースする

JSONを扱っているとたまにこのような連想配列の文字列に出くわすことがある。

{
  "fruits": {
    "1": {
      "key": 1,
      "value": [
        "apple",
        "りんご",
        150,
        "青森県"
      ]
    },
    "2": {
      "key": 2,
      "value": [
        "melon",
        "メロン",
        600,
        "茨城県"
      ]
    },
    "3": {
      "key": 3,
      "value": [
        "watermelon",
        "スイカ",
        1400,
        "熊本県"
      ]
    }
  }
}

valueの中に文字列だろうが数値だろうが放り込まれている気持ち悪いパターン。C#のように型付けありきの言語だと強烈に違和感があるが、JavaScriptの仕様的にこれはまかり通ってしまう。 配列の中に何でもかんでもぶち込まなくても、実は連想配列がある場合すらライブラリによってはパースに苦労することがある

DynamicJSONの場合

JSONを扱っていると大変お世話になるライブラリだが(軽いし!)、連想配列はなぜかうまくいかない。例えば、このようにテンプレートクラスを定義してパースすると、

using System.IO;
using Codeplex.Data;

class Program
{
    static void Main(string[] args)
    {
        var jsonStr = File.ReadAllText("json1.json");//JSON文字列を読み込む

        var jsonObj = DynamicJson.Parse(jsonStr);//DynamicJSONで読み込み
        RootJson fruits = jsonObj.Deserialize<RootJson>();

        foreach(var f in fruits.fruits)
        {
            Console.WriteLine(f.ToString());
        }
        //実行結果
        //続行するには何かキーを押してください . . .
    }
}

public class RootJson
{
    public Dictionary<int, Fruit> fruits { get; set; }

    public class Fruit
    {
        public int key { get; set; }
        public object[] value { get; set; }

        public override string ToString()
        {
            return "key : " + key + ", value : [" + string.Join(", ", value) + "]"; 
        }
    }
}

なにも表示されない……!?ブレークポイントで止めると…明らかにパースに失敗してるよね。 f:id:enjyuu:20170707221505p:plain うまい方法あるのかもしれないがちょっとこれはいただけない。ちなみにfruitsの定義をDictionary<string, Fruit>に変えてもダメ。

Json.NETの場合

これはすんなり行く。DynamicJSONと同じテンプレートクラスを使っても、

using System.IO;
using Newtonsoft.Json;

class Program
{
    static void Main(string[] args)
    {
        var jsonStr = File.ReadAllText("json1.json");//JSON文字列を読み込む

        var fruits = JsonConvert.DeserializeObject<RootJson>(jsonStr);

        foreach(var f in fruits.fruits)
        {
            Console.WriteLine(f.ToString());
        }
        /*
        実行結果
        [1, key : 1, value : [apple, りんご, 150, 青森県]]
        [2, key : 2, value : [melon, メロン, 600, 茨城県]]
        [3, key : 3, value : [watermelon, スイカ, 1400, 熊本県]]
        続行するには何かキーを押してください . . .
         */
    }
}

望んだ通りの結果に。

DataContractJsonSerializerの場合

ちなみに.NET付属のDataContractJsonSerializer(System.Runtime.Serialization.Json)を使う場合は例外が発生

using System.IO;
using System.Runtime.Serialization.Json;

class Program
{
    static void Main(string[] args)
    {
        var serializer = new DataContractJsonSerializer(typeof(RootJson));
        RootJson fruits;
        using(var sr = new StreamReader("json1.json", Encoding.GetEncoding("UTF-8")))
        {
            fruits = serializer.ReadObject(sr.BaseStream) as RootJson;
        }

        foreach(var f in fruits.fruits)
        {
            Console.WriteLine(f.ToString());
        }
    }
}

ハンドルされていない例外: System.Runtime.Serialization.SerializationException: オブジェクト 型 RootJson の のシリアル化を解除しているときにエラーが発生しました 。予期しない文字 ‘i’ が見つかりました。 —> System.Xml.XmlException: 予期しない 文字 ‘i’ が見つかりました。 場所 System.Xml.XmlExceptionHelper.ThrowXmlException(XmlDictionaryReader read er, XmlException exception) 場所 System.Runtime.Serialization.Json.XmlJsonReader.ReadAttributes() 場所 System.Runtime.Serialization.Json.XmlJsonReader.ReadNonExistentElementNa me(StringHandleConstStringType elementName) 場所 System.Runtime.Serialization.Json.XmlJsonReader.Read() 場所 System.Xml.XmlBaseReader.IsStartElement() 場所 System.Xml.XmlBaseReader.IsStartElement(XmlDictionaryString localName, X mlDictionaryString namespaceUri) 場所 System.Runtime.Serialization.XmlReaderDelegator.IsStartElement(XmlDictio naryString localname, XmlDictionaryString ns) 場所 System.Runtime.Serialization.XmlObjectSerializer.IsRootElement(XmlReader Delegator reader, DataContract contract, XmlDictionaryString name, XmlDictionary String ns) 場所 System.Runtime.Serialization.Json.DataContractJsonSerializer.InternalIsS tartObject(XmlReaderDelegator reader) 場所 System.Runtime.Serialization.Json.DataContractJsonSerializer.InternalRea dObject(XmlReaderDelegator xmlReader, Boolean verifyObjectName) 場所 System.Runtime.Serialization.XmlObjectSerializer.InternalReadObject(XmlR eaderDelegator reader, Boolean verifyObjectName, DataContractResolver dataContra ctResolver) 場所 System.Runtime.Serialization.XmlObjectSerializer.ReadObjectHandleExcepti ons(XmlReaderDelegator reader, Boolean verifyObjectName, DataContractResolver da taContractResolver) — 内部例外スタック トレースの終わり —

Stream経由で書かなきゃいけないうえ、System.Runtime.Serializationの参照も追加しないといけないので面倒。第一例外になるしこれは正直ないかな

結論:素直にJson.NETを使おう

任意の文字の繰り返し回数を検出する

特定の文字列の繰り返しを検出したいなら正規表現を使うのが王道だが、繰り返す文字列が任意だと一筋縄にはいかないことがある。例えば、

hogehogeeeeehoggggggge

という文字列があったとして、2文字以上連続した箇所とその文字と繰り返し回数を検出したい場合(この例では「eeeee」と「ggggggg」)。また、繰り返される文字列はわからない(ここでは簡略化のためにa-zとするが)ものとする。

ダメな例

まずダメな例から。

var text = "hogehogeeeeehoggggggge";
var matches = text.match(/([a-z]){2,}/g);

document.write(matches);//hogehogeeeeehoggggggge 

一見うまくいきそうだが、[a-z]にテキストの箇所以外の文字列が含まれているため全体がマッチしてしまう。これではダメ。

うまくいく一例

後方参照の「\1」を使うとうまくいく。

\n n には正の整数が入ります。正規表現内で (左括弧を数えて) n 番目の括弧でくくられた部分に該当する部分文字列を後方参照します。

例えば、/apple(,)\sorange\1/ は “apple, orange, cherry, peach” の ‘apple, orange,’ にマッチします。具体的なサンプルをこの表の後に掲載しています

RegExp - JavaScript | MDN

var text = "hogehogeeeeehoggggggge";
var matches = text.match(/([a-z])\1+/g);

document.write(matches);//eeeee,ggggggg  

2回以上繰り返しなので「\1+」、3回以上繰り返しなら「\1\1+」とする。ひたすら\1を繰り返すのは頭悪いので、後方参照と繰り返しのと{}を組み合わせてもOK。例えば、「([a-z])\1{5,}」なら6回以上繰り返しとなり、「eeeee」は引っかからなくなる。

上限が必要な場合は、例えば{1,5}で繰り返し2回以上6回以下のみになり、結果は「eeeee, gggggg」となる。厳密にg×7は除外されないので、繰り返しの上限が必要な場合はもうひと工夫いりそうだ。

replaceの引数に関数指定

繰り返し回数はどう判定すべきだろうか?この例だとマッチした箇所の文字数を数えれば終わりだが、キャプチャする文字数が可変の場合は困ったことになる。一例ではあるが、string.replaceの引数に関数を指定する例が使えそうだ。

String.prototype.replace() - JavaScript | MDN

var text = "hogehogeeeeehoggggggge";
var replace = text.replace(/([a-z])\1+/g, function (match, p1, offset, string) {
    document.writeln("match : " + match + ", p1 : " + p1 + ", offset : " + offset + ", string : " + string);
    return match;//何もしない置き換え
})
document.writeln(replace);
//match : eeeee, p1 : e, offset : 7, string : hogehogeeeeehoggggggge 
//match : ggggggg, p1 : g, offset : 14, string : hogehogeeeeehoggggggge 
//hogehogeeeeehoggggggge

メソッドの引数の仕様は上記リンクを参照。「return match」とすることで、置き換えメソッドであるが、実質何もしないメソッドとすることが可能である。

replaceを使用した簡単な文字列圧縮・解凍

限定した用途であるが、aaa→a3のように繰り返し回数を数字で置き換えれば、文字列の簡易圧縮・解凍メソッドを作ることができる。

var originalStr = "hogehogeeeeehoggggggge";

//圧縮
function deflate(str) {
    return str.replace(/([a-z])\1+/g, function (match, p1) {
        var cnt = match.length / p1.length;
        if (match.length % 1 == 0) return p1 + cnt;
        else return match;
    })
}
//解凍
function inflate(str) {
    return str.replace(/([a-z])(\d+)/g, function (match, p1, p2) {
        return Array(Number(p2) + 1).join(p1);
    })
}

var str1 = deflate(originalStr);
document.writeln(str1);//hogehoge5hog7e
var str2 = inflate(str1);
document.writeln(str2);//hogehogeeeeehoggggggge 

もしかしたらどこかで使えるかもしれない…?