Emotion Wave Tech Blog

福岡にあるエモーションウェーブ株式会社のエンジニアが書いています。

ソフトの焼き直し?!果たして工数はどのくらい?

古くから使われているVB6で作られたソフトを運用をしています。

もう20年近く前に作られたソフトです。 Visual Basicバージョン6.0は1998年にリリースされ、Windows上で動くソフトウェアを簡単に作成することができましたので、2000年前後には広く利用されていました。

そのようなシステムがついに焼き直しの議論が浮上し、突如として「.NETへの焼き直しの概算を今日中に提出してほしい」という依頼がありました。

概算を出すためには、どのような手順を踏むべきでしょうか。 様々な手法が考えられますが、私が実施した手順を紹介いたします。

この手順で、当日中に提出でき、見積もりにも納得していただけました。

まず、3つのステップで見積もりを行うことにしました。概算工期も示しました。

1. 現在の規模を把握する。

まず、このVB6で作成されたアプリケーションのソースコードの行数を調査しました。 VB6のソースコードをカウントするツールはいくつかありますが、今回は「かぞえチャオ!」を使用しました。

カウント対象の拡張子は、basとcls、frmです。 これにより、総ステップ数とコメントを除いた実際のステップ数が確認できます。

なお、frmは画面の構成要素を表すもので、画面の部品の設定情報が含まれており、htmlのようなものです。ロジックとは異なりますが、焼き直し作業では画面オブジェクトの把握やイベントの解析が必要ですので、frmも含めてカウントすることにしました。

調査の結果、実ステップ数合計は約40,000行でした。

2. 新しい言語で焼き直した場合の規模を把握する。

VB6から.NETに焼き直した場合、どのくらいのステップ数で実現できるのかを探るために、ブログ記事などを調査しました。

いくつかの記事によると、VB6ではデータベース接続やテキストボックスなどのコントロール操作のコードが肥大化しやすかったのに対し、.NETはそのような点で改善されており、半分のコード量で同様の機能を実現できると書いてありました。

しかし、今回のVB6アプリケーションはバックグラウンド処理が多く、画面UIや帳票はそれほど複雑ではありません。 そのため、この削減効果を十分に享受できないと考え、コード量が約30%削減できると予想しました。

したがって、焼き直し後の見積もりステップ数は28,000行となります。

3. その規模から、どのくらいの工数が必要かを計算する。

では、28,000行の規模にはどのくらいの工数が必要なのでしょうか。

「ソフトウェア開発分析データ集2020」には、設計やテストを含めて1人月あたり約740ステップをこなせるという記載がありました(「5. 参考にした資料」を参照)。

この情報を元に計算すると、37人月が必要となります。

設計、製造、テストなどで、計37人月工数が必要という結果になりました。

4. 工期

次に、開発期間について考えます。 「標準工期(月数)=投入工数の立方根×2.5」という式があります。

37の立方根はおよそ3.3となります。 この値に2.5を掛けると8.25となります。 したがって、開発期間はおよそ8ヶ月となります。

私自身もこのアプリケーションの全体像を把握していますが、直感的にこの程度の工数がかかるだろうと感じています。

以上の見積もりを報告し、計算の過程を説明したところ、納得していただけました。

5. 参考にした資料

「ソフトウェア開発分析データ集2020」はIPAのHPよりダウンロード可能です。 P84の A1.2 SLOC 規模別 SLOC 生産性 という章を参考にしています。 https://www.ipa.go.jp/digital/chousa/metrics/ug65p90000001iu7-att/000085879.pdf

「40KSLOC 未満」かつ「KSLOC/160人時」の行で、「平均」列の値が、0.74つまり740ステップでした。 これを参考にしました。

ちなみに最新の2022が出てました(後から気づいた)。 最新のほうが少々生産性が下がっていて、640ステップでしたね。 それで提出すれば良かったかな…。

Youtubeライブ配信リストをつくる【Googleサイト編】

目次

Youtubeライブ配信を一覧で見ることができるサイトをつくる

どうも、高島です。

Youtubeでゲーム実況など生配信していることはみなさんご存知だと思います。 最近はスポーツの生中継も意外にあります。

例えば、日本アマチュアサッカー界の最高峰JFLオートバイレースのロードやモトクロス、ボクシングなど、楽しめるコンテンツが配信されています。

無料で見ることができるので、嬉しいですね。

ライブ配信だけを集めて、画面上で選べるものがほしいと思いまして、Googleのウェブサイト作成サービスでお手軽に作ってみました。

youtubeからデータを受け取るには?

youtube APIというものがあります。 このapiで検索条件を指定すれば、希望の動画一覧を取得できます。 このAPIPHPPythonRuby などで使えますが、URLにパラメータ指定でも使うことができます。 URLで使うのがお手軽ですので、今回はこの方法を使います。

youtube APIを利用できるようにする

APIを使うには、APIの利用申請し、API キーを発行する必要があります。 このあたりはたくさんのブログで紹介されているので、「youtube apiキー」でgoogle検索して参照してください。 UIもコロコロ変わっているようですので、できるだけ最新の記事を見つけてください。

youtube APIで動画検索する方法

公式ページで使い方を確認してみましょう。 developers.google.com

https://www.googleapis.com/youtube/v3/search にgetパラメータを付けることで動画情報を取得できます。

必要なパラメータは、

  • 取得情報(idかsnippetか。今回は詳細情報は要らないのでid)

  • 取得件数(とりあえず20件)

  • 並び順(とりあえず日付が新しいものから)

  • タイプ(videoかchannelかplaylistか。今回はvideo)

  • 先程発行したAPIキー

  • 検索したいYouTubeチャンネルのチャンネルID(とりあえずサッカーJFLチャンネル)

  • イベントタイプ(今現在配信中は、live。配信前はupcoming、配信を終えたものはcompletedです)

JFL日本フットボールリーグ)の現在生配信中の動画を取得するには、このようになります。

https://www.googleapis.com/youtube/v3/search?part=id&maxResults=20&order=date&type=video&key={発行したAPIキー}&channelId=UCIqR8JZTxJScZg3H4l_Rfhg&eventType=live

結果はjsonで返って来ます。 画面表示で必要なのは"videoId"です。

[
    {
        "kind": "youtube#searchResult",
        "etag": "YSgQNp77JrlZMRUzY9My3C95I58",
        "id": {
            "kind": "youtube#video",
            "videoId": "v_6INYqSU6g"
        }
    },
    {
        "kind": "youtube#searchResult",
        "etag": "HOFJ_q3JZPlMSXZgKynZUggBD0U",
        "id": {
            "kind": "youtube#video",
            "videoId": "fNECO8zkvvo"
        }
    },
    {
        "kind": "youtube#searchResult",
        "etag": "I_f_N-fpVLfqIysKxkJZSuFdwNA",
        "id": {
            "kind": "youtube#video",
            "videoId": "o-IuYxLwrSk"
        }
    }
]

動画をHTMLページに貼り付ける。

動画をブラウザに表示するには?

これは簡単です。 iframeのsrcに指定するだけです。

<iframe src="https://www.youtube.com/embed/{動画ID} frameborder="1" sandbox="allow-scripts allow-popups allow-forms allow-same-origin allow-popups-to-escape-sandbox allow-downloads allow-modals" allowfullscreen></iframe>

実際のソース

live、upcoming、completedの順で取得するサンプルです。 idが"youtubeList"のdivタグに動画iframeを追加していきます。

<!-- jQueryによるスクリプト -->
<script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
<script type="text/javascript">
$(function() {
    const base_youtube_url = 'https://www.googleapis.com/youtube/v3/search?part=id&maxResults=20&order=date&type=video&key=AIzaSyDHHcO1rMsZJXcc5t-oJwlGGHBeNs3nf7U';

    const promiseFunc = (value,value2) => {
      return new Promise((resolve, reject) => {
        console.log(value + value2);
        $.ajax({
          type: 'GET',
          url: value + value2,
          datatype: 'json',
          success: function(json){
            resolve(json.items);
          },
          error: function(){
            reject();
          }
        });
      });
    };

    // 非同期関数
    async function asyncFunc() {
      // 並行して処理が実行され、全ての処理が終わるまで待機
      const values = await Promise.all([
        promiseFunc(base_youtube_url,'&channelId=UCIqR8JZTxJScZg3H4l_Rfhg&eventType=live'),
        promiseFunc(base_youtube_url,'&channelId=UCIqR8JZTxJScZg3H4l_Rfhg&eventType=upcoming'),
        promiseFunc(base_youtube_url,'&channelId=UCIqR8JZTxJScZg3H4l_Rfhg&eventType=completed')
      ]);

      const jsonitem = values.flat();
      console.log(jsonitem);

      num = jsonitem.length;
      for(var i = 0; i < num ; i++){
        var ID = jsonitem[i].id.videoId;
        $("#youtubeList").append('<iframe src="https://www.youtube.com/embed/' + ID + '" frameborder="1" sandbox="allow-scripts allow-popups allow-forms allow-same-origin allow-popups-to-escape-sandbox allow-downloads allow-modals" allowfullscreen></iframe>');
      }
    }

    asyncFunc();
});

</script>
<div id="youtubeList"></div>

Googleサイトで作ってみる

Googleサイト作成ページにアクセスしてみましょう。 sites.google.com

新しいサイトの作成から「空白」を選びます

作成ページに飛ぶので、プロジェクト名やページタイトルを入力しましょう。

画面右側の「埋め込む」を選びます。

「埋め込みコード」の部分に先程のjavaScriptを書き込みましょう。

埋め込みプレビューが表示されるので、youtube動画の一覧が表示されているのを確認して「挿入」を押します。

埋め込みの領域の縦横を調整しましょう。

画面右上のディスプレイマーク(赤印)のところをクリックするとプレビューで確認できます。 人マーク(黄色)でリンクURLを取得できます。リンクURLで自分のスマホから見ることができます。

スマホではこのようなレイアウトです。

このように試合中継を見ることができます。

あとがき

ライブ配信一覧を表示するサイトはいかがだったでしょうか。

サイトはドラッグ&ドロップとコード貼り付けだけで簡単に作ることができました。

ただし、実はこの方法は弱点があり、ページを表示するたびにYouTubeAPIの検索が走るので、クォータという検索回数制限みたいなものに引っかかってしまいます。

そのため、実際はlive、upcoming、completedを全て取得するのではなく、liveのみで件数を少なくするほうが良いでしょう。

それでも他人に公開してしまうと、あっという間に制限を超えてしまうことになります。

次回は、googleスプレッドシートに定期的にライブ配信リストを書き込み、それを元に表示するサイトを作ってみたいと思います。

ChromeのIEタブで開くショートカット

目次

IEが終わる、うちのシステムIEしか対応してない!

どうも、高島です。

IEのサポートが2022年6月に終わろうとしています。

最新のWindowsだとデフォルトでインストールすらされていない状況です。

けれども古いシステムだと、むしろIEでしか動かないというサイトも多いと思います。

Microsoft EdgeIEモードを利用するか、Chrome拡張機能であるIETabで回避するしかありません。

とはいえ、「このサイトはIEモードで利用する」という設定を各PCでしないとならず、部署全員のPCでそれをするのは負担です。

そこで、「ChromeIEタブでこのサイトを開け!」というショートカットを作りたいと思います。

まず、IEタブの拡張機能をインストール。

これは、各PCでやってもらわないといけないです。

Chromeの検索窓で、「Ietab」と打てば先頭に出てくるので、Chromeに追加してもらうだけです。

あとietabhelper.exeもインストールが必要ですが、画面の指示に従えばできます。

コマンドプロンプトIEタブ起動をしてみる。

コマンドプロンプトを開いて、こう打ってみましょう。

start chrome chrome-extension://hehijbfgiekmjfkfjpbkbammjbdenadd/nhc.htm#url=https://www.yahoo.co.jp

Chromeが起動し、yahooがIEとして表示されています。 f:id:devew:20220401115829p:plain

ショートカットを作ってみよう。

デスクトップで右クリックメニューから「ショートカット作成」を選んでください。 f:id:devew:20220401120240p:plain

「項目の場所を入力してください」に、こう打ち込みましょう。

C:\Windows\system32\cmd.exe /c start chrome chrome-extension://hehijbfgiekmjfkfjpbkbammjbdenadd/nhc.htm#url=https://www.yahoo.co.jp

ショートカットで立ち上げるのは、コマンドプロンプトです。

それにさっき試したコマンドを渡します。

これで、yahooをIEタブで立ち上げるショートカットができました。

htmlテーブルをレスポンシブに

目次

画面幅に合わせて配置が変わるテーブル

どうも、高島です。

表を、画面サイズに合わせて狭めたり広げてみせたり、ということを試してみました。 画面幅に合わせて、3列、2列、1列と変化します。

f:id:devew:20200619192111g:plain

これ、IEでは正常に動きませんでした。 Chromeでは動きました。あしからず。

ソースはこんな感じです。

<!doctype html>
    <head>
       <meta charset="utf-8">
       <meta http-equiv="X-UA-Compatible" content="IE=edge">
       <title>ファイルアップロード</title>
       <style type="text/css">
table{
    max-width: 1139px;
    width:100%;
}

table,th,td{
    border-collapse: collapse;
    border:1px solid #333;
    vertical-align: baseline;
    margin:0;
    padding:0;
}


table tr{
    display: inline;
    float: left;
}


table th{
    background-color: #CCCCCC;
}

.department{
    width:80px;
}

.num{
    width:80px;
}

.name{
    width:80px;
}

.position{
    width:120px;
}

.detail.department{
    background-color: #CCCCCC;
}

        </style>
       <script
          src="https://code.jquery.com/jquery-1.12.4.min.js"
          integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="
          crossorigin="anonymous"></script>

       <script>
$(document).ready(function(){

   // 初期表示処理
   tableResize();

   //リサイズされたときの処理
   $(window).resize(function() {

       // ダミー行を消し、隠し列を表示に戻す
       $('#datatable tr[data-department="dummy"]').remove();
       $('#datatable td.department').css('display','table-cell');

       tableResize();
   });


   // リサイズ時イベント
   function tableResize(){
       // テーブル幅をtr幅で割る
       switch (Math.floor($("#datatable").width() / 370)) {
         case 0:
         case 1:
           console.log('1行に1人表示');
           break;
         case 2:
           console.log('1行に2人表示');
           addDummyTr(2);
           break;
         case 3:
         default:
           console.log('1行に3人表示');
           addDummyTr(3);
           break;
       }
   }

   function addDummyTr(humanCnt){

       // ヘッダ部調整用の仮行
       var DUMMY_DETAILHEADER_TMPL ='<tr data-department="dummy"><th class="header department">所属</th><th class="header num">社員番号</th><th class="header name">氏名</th><th class="header position">役職</th></tr>';

       var dummy_detailheader = "";

       // ヘッダに調整用の行を追加する。
       for (var i = 1; i < humanCnt; i++) {
           dummy_detailheader = dummy_detailheader + DUMMY_DETAILHEADER_TMPL;
       }

       $('#datatable thead tr:last').after(dummy_detailheader);

       // ヘッダ2番目以降の所属列を消す
       $('#datatable thead th.department:gt(0)').css('display','none');

       // 明細部調整
       // まず、所属を全て取得し一意にする。
       var departmentList = [];
       $('#datatable tbody tr').each(function(index, element){
           departmentList.push($(element).data('department'));
           
       });
       departmentList = departmentList.filter(function (x, i, self) {
         return self.indexOf(x) === i;
       });

       console.log('departmentList:' + departmentList);


       var DUMMY_DETAILROW_TMPL ='<tr data-department="dummy"><td class="department"></td><td class="num" style="color: transparent;">dummy</td><td class="name"></td><td class="position"></td></tr>';

       // 例として、1行に2人の場合は、所属1人だとスキマができるため、スキマ埋めのためのdummy行を追加する。
       for(let v of departmentList) {

           // スキマ埋めすべきdummy行数を求める。
           let addDummyRowCnt = 0;
           let departmentLength = $('#datatable tbody tr[data-department="' + v + '"]').length
           if(departmentLength <= humanCnt) {
               // 例:所属2人で、1行に3人表示の場合は、1人分のスキマ埋めが必要。
               addDummyRowCnt = humanCnt-departmentLength
           } else {
               // 例:所属4人で、1行に3人表示の場合は、2行になるため、この所属の必要な領域は3×2=6となる。2人分のスキマ埋めが必要。
               addDummyRowCnt = (Math.ceil(departmentLength / humanCnt) * humanCnt) % departmentLength;
           }

           // スキマ分、ダミー行を追加する。
           for(let i = 0; i < addDummyRowCnt; i++) {
               $('#datatable tbody tr[data-department="' + v + '"]:last').after(DUMMY_DETAILROW_TMPL);
           }
       }
           

       // 1行に2人以上の場合、表示上の左から1番目の所属列は残し、後の所属列は消す。
       if(humanCnt > 1){

           // 1行あたりの表示人数の倍数ではない行(見た目上、右側に移った行)は所属列を消す
           $('#datatable tbody tr').filter(function(index) {
               return index % humanCnt >= 1;
           }).find('td.department').css('display','none');

       }
   }
});

       </script>
   </head>
    <body>
        <form action="" method="post">
            <table id="datatable">
                <thead>
                    <tr>
                        <th class="header department">所属</th>
                        <th class="header num">社員番号</th>
                        <th class="header name">氏名</th>
                        <th class="header position">役職</th>
                    </tr>
                </thead>
                <tbody>
                    <tr data-department="tokyo">
                        <td class="detail department">東京</td>
                        <td class="detail num">3</td>
                        <td class="detail name">田中</td>
                        <td class="detail position">課長</td>
                    </tr>
                    <tr data-department="tokyo">
                        <td class="detail department">東京</td>
                        <td class="detail num">11</td>
                        <td class="detail name">鈴木</td>
                        <td class="detail position">主任</td>
                    </tr>
                    <tr data-department="tokyo">
                        <td class="detail department">東京</td>
                        <td class="detail num">16</td>
                        <td class="detail name">内海</td>
                        <td class="detail position">パート</td>
                    </tr>
                    <tr data-department="tokyo">
                        <td class="detail department">東京</td>
                        <td class="detail num">5</td>
                        <td class="detail name">田口</td>
                        <td class="detail position">パート</td>
                    </tr>
                    <tr data-department="nagoya">
                        <td class="detail department">名古屋</td>
                        <td class="detail num">14</td>
                        <td class="detail name">富田</td>
                        <td class="detail position">主任</td>
                    </tr>
                    <tr data-department="osaka">
                        <td class="detail department">大阪</td>
                        <td class="detail num">7</td>
                        <td class="detail name">松永</td>
                        <td class="detail position">主任</td>
                    </tr>
                    <tr data-department="osaka">
                        <td class="detail department">大阪</td>
                        <td class="detail num">14</td>
                        <td class="detail name">井ノ瀬</td>
                        <td class="detail position">パート</td>
                    </tr>
                    <tr data-department="fukuoka">
                        <td class="detail department">福岡</td>
                        <td class="detail num">21</td>
                        <td class="detail name">牧田</td>
                        <td class="detail position">主任</td>
                    </tr>
                    <tr data-department="fukuoka">
                        <td class="detail department">福岡</td>
                        <td class="detail num">6</td>
                        <td class="detail name">水島</td>
                        <td class="detail position">リーダー</td>
                    </tr>
                    <tr data-department="fukuoka">
                        <td class="detail department">福岡</td>
                        <td class="detail num">16</td>
                        <td class="detail name">堀川</td>
                        <td class="detail position">パート</td>
                    </tr>
                    <tr data-department="kagosshima">
                        <td class="detail department">鹿児島</td>
                        <td class="detail num">27</td>
                        <td class="detail name">大迫</td>
                        <td class="detail position">主任</td>
                    </tr>
                </tbody>
            </table>
        </form>
    </body>
</html>

考え方

  • テーブルのtrタグをinlineにし、行を横並びに出来るようにする。
  • 所属ごとに人数が違うので、javascriptに動的に空白行を追加したりして、ちょうどよい見た目にする。

やり方

ポイントは、cssでtrタグをdisplay: inline;にしていること。

table tr{
    display: inline;
    float: left;
}

これでtrが横に並びます。 画面横幅に合わせて勝手に折り返してくれます。

とはいえ、所属によって人数が異なるので、名古屋所属の人が東京の行に表示されたりしておかしくなります。 そこで空白行を埋めることで、調整します。

テーブルの幅を検知して、何列表示になるのか判断します。

   // リサイズ時イベント
    function tableResize(){
        // テーブル幅をtr幅で割る
        switch (Math.floor($("#datatable").width() / 370)) {
          case 0:
          case 1:
            console.log('1行に1人表示');
            break;
          case 2:
            console.log('1行に2人表示');
            addDummyTr(2);
            break;
          case 3:
          default:
            console.log('1行に3人表示');
            addDummyTr(3);
            break;
        }
    }

trにはデータ属性departmentを定義しており、どこの所属かわかるようにしています。 javascriptのaddDummyTr関数内で、所属ごとの人数から、必要な空行を追加します。

f:id:devew:20200619191833p:plain

f:id:devew:20200619191937p:plain

横幅に合わせて、ちょうどよくなるように、空白行が追加されました。

次に、ヘッダ行も表示します。 ヘッダのtrタグの後ろに、ヘッダをさらに追加してしまいます。 1行に2人表示する画面幅なら、ヘッダを1行追加し、 3人表示するなら、ヘッダを2行追加します。

   // ヘッダ部調整用の仮行
    var DUMMY_DETAILHEADER_TMPL ='<tr data-department="dummy"><th class="header department">所属</th><th class="header num">社員番号</th><th class="header name">氏名</th><th class="header position">役職</th></tr>';

    var dummy_detailheader = "";

    // ヘッダに調整用の行を追加する。
    for (var i = 1; i < humanCnt; i++) {
        dummy_detailheader = dummy_detailheader + DUMMY_DETAILHEADER_TMPL;
    }

    $('#datatable thead tr:last').after(dummy_detailheader);

f:id:devew:20200619191958p:plain

f:id:devew:20200619192014p:plain

これでヘッダも付きました。

所属列は要らないので消します。

   // ヘッダ2番目以降の所属列を消す
    $('#datatable thead th.department:gt(0)').css('display','none');

departmentはヘッダの所属列についたクラスです。 gt(0)で、2番目以降が選択対象になります。 それをdisplay:noneにして見えなくします。

これで完成です。

あとがき

同じことを実現するにも、様々なやり方があるでしょうし、外部のモジュールを取り入れたら、あっさり実現できたと思います。

今回の書き方が、きれいな正当なやり方だとは思っていませんが、お勉強兼ねてやってみました。

【javascript】2つの連想配列を比較し、重複するものを返す

目次

javascript連想配列を比較し、重複するものがないか、調べたい

どうも、高島です。

javascriptで2つの配列があります。 両方とも存在するデータを取り出すには、どうすればいいでしょうか?

普通の配列を比較するサンプルは、ネット上に結構あるのですが、連想配列の例があまり無かったので、記事を書きました。

早速ですが、filterの中でfilterを呼べば、可能です。

const ary1 = [
  {"keyno":"A1","room":"101"},
  {"keyno":"A1","room":"102"},
  {"keyno":"B1","room":"201"},
  {"keyno":"C1","room":"301"},
  {"keyno":"D1","room":"401"},
];


const ary2 = [
  {"keyno":"A1","room":"101"}, 
  {"keyno":"C3","room":"301"},
  {"keyno":"D1","room":"401"},
  {"keyno":"E1","room":"501"},
];

// 重複有り
var reList = ary1.filter(ary1row=>ary2.filter(
    ary2row=>
        ary2row.keyno === ary1row.keyno &&
        ary2row.room === ary1row.room).length > 0);
        
console.log("両方とも存在するデータ");
console.log(reList);

結果はこうです。

両方とも存在するデータ
[
  { keyno: 'A1', room: '101' },
  { keyno: 'D1', room: '401' }
]

filter内はどんな動きなのか?

実際はどんな処理がされているのでしょうか? console.logを埋め込んで確認してみます。

// 内部の動作を確認してみる
ary1.filter(ary1row=>{
    console.log("ary1row.room=" + ary1row.room);
    ary2.filter(
        ary2row=>{
            console.log("    ary2row.room=" + ary2row.room);
        })
});

結果はこうなりました。

ary1row.room=101
    ary2row.room=101
    ary2row.room=301
    ary2row.room=401
    ary2row.room=501
ary1row.room=102
    ary2row.room=101
    ary2row.room=301
    ary2row.room=401
    ary2row.room=501
ary1row.room=201
    ary2row.room=101
    ary2row.room=301
    ary2row.room=401
    ary2row.room=501
ary1row.room=301
    ary2row.room=101
    ary2row.room=301
    ary2row.room=401
    ary2row.room=501
ary1row.room=401
    ary2row.room=101
    ary2row.room=301
    ary2row.room=401
    ary2row.room=501

ary1をループして、さらにその中でary2をループしていることがわかります。 filterを使えば、ループを書かなくて済む、というわけです。

冒頭の重複を取り出す処理に話を戻します。

ary2.filter関数が、ary1rowとary2rowを比較し、一致する行を返してくれます。 この部分ですね。

ary2.filter(
    ary2row=>
        ary2row.keyno === ary1row.keyno &&
        ary2row.room === ary1row.room)

ary2.filterは、比較条件がtrueならば、データを返します。 もしfalseなら何も返しません。

データを返ってきたか否かは、lengthで判断します。

もうちょっとわかりやすく、returnを省略せずに、lengthでのtrue/false判定を分離して書いてみます。

var reList = ary1.filter(ary1row=>ary2.filter(
    ary2row=>
        ary2row.keyno === ary1row.keyno &&
        ary2row.room === ary1row.room).length > 0);

↓↓↓↓↓↓

var reList = ary1.filter(ary1row=>{
    var reAry2row = ary2.filter(
        ary2row=>{
            return (ary2row.keyno === ary1row.keyno &&
                    ary2row.room === ary1row.room)
        });

    return reAry2row.length > 0 ;
});

ary1rowとマッチしたreAry2row が存在すれば、length > 0 、ですのでtrueで返します。 ary1.filter()はtrueとなったary1rowをreListに返します。

こうして重複したデータのみが reListにセットされます。

2つの配列を比較し、重複しないものを返す

では、ary1に存在するがary2に存在しないデータは?

これは簡単です。結果が0件のものですね。 length === 0 に変えます。

// 重複しない
reList = ary1.filter(ary1row=>ary2.filter(
    ary2row=>
        ary2row.keyno === ary1row.keyno &&
        ary2row.room === ary1row.room).length === 0);

console.log("ary1に存在するがary2に存在しないデータ");
console.log(reList);
ary1に存在するがary2に存在しないデータ
[
  { keyno: 'A1', room: '102' },
  { keyno: 'B1', room: '201' },
  { keyno: 'C1', room: '301' }
]

(おまけ)自分自身の配列内で重複を取り出す

これまでは、異なる配列同士を比較しましたが、 今度は自分自身と比較すればいいです。

const ary1 = [
  {"keyno":"A1","room":"101"},
  {"keyno":"A1","room":"102"},
  {"keyno":"B1","room":"201"},
  {"keyno":"C1","room":"301"},
  {"keyno":"A1","room":"101"},
  {"keyno":"C1","room":"301"},
  {"keyno":"D1","room":"401"},
];


// 重複有り
var reList = ary1.filter((ary1row,ary1idx,selfary)=>selfary.filter(
    ary2row=>
        ary2row.keyno === ary1row.keyno &&
        ary2row.room === ary1row.room).length > 1);
        
console.log("重複するデータ");
console.log(reList);
重複するデータ
[
  { keyno: 'A1', room: '101' },
  { keyno: 'C1', room: '301' },
  { keyno: 'A1', room: '101' },
  { keyno: 'C1', room: '301' }
]

filter() はcallback関数を引数にとります。 このcallback関数の第3引数は自分自身の配列です。 それと比較すればいいです。

まとめ

filter内でfilterを呼び、そこで比較すれば、二重ループを書く必要はなくなります。

最初にfilter見たときは訳わかりませんでしたが、理解するとスッキリ書けます。

欠点といえば、読むには慣れが必要なことでしょうか。

人が書いたものは、正直ちょっとわかりにくいことが多いです。

var reList = ary1.filter((x,y,z)=>z.filter(
    w=>
        w.keyno === x.keyno &&
        w.room === x.room).length > 1);

アロー関数は、こんな感じで略した変数名で書くことが多いので、変数名からは内容が推測できませんし、returnを省略して書かれると、初心者はコードが読めずに苦しんだりします。

もう慣れるしかないんでしょうけどね。