20210911のJavaScriptに関する記事は13件です。

【JavaScript】入力フォームを使ったHTMLページをスッキリした見た目にする

コードだけをみたい人は最後に全体を載せてますのでそちらへ。 それでは本題の前に、 まずは普通にhtmlでselect要素1つ、input-text要素1つを設置します。 <!doctype html> <html> <head> <meta charset="UTF-8"> <title>text and select form</title> </head> <body> <select> <option value="2021">2021</option> <option value="2022">2022</option> <option value="2023">2023</option> </select>年 <br><br> <input type="text"> </body> </html> 上のHTMLをブラウザで見るとselect要素とinput-text要素が表示されます。 これらを多用して「入力ページ」を作るのはいいのですが、 そのページを印刷してそのまま利用するわけにはいきません。 フォームを使ったページを印刷すると、 行がズレる、フォント・大きさ・ベースラインが合わない、見た目のイメージが変わる、 といったことが起こるからです。 HTMLの入力フォームは印刷してみるとWYSIWYGじゃないんです。 そこで、 「入力フォームを使ったHTMLページをJavaScriptでスッキリした見た目にする」 ということを考えました。 目指すのは、 文字列クリック → 入力フォームが出現 → フォーム入力終了 → フォーム消滅 → 文字列に戻る です。 まずは、HTMLを書き換えます。 <!doctype html> <html> <head> <meta charset="UTF-8"> <title>text and select form</title> </head> <body> <!--spanで囲みます--> <span>2021</span>年 <br><br> <!--spanで囲みます--> <!--text入力変更前の文を適当に--> <span>textを書き換えます</span> </body> </html> 上のHTMLをブラウザで見ると「2021年」「textを書き換えます」がそれぞれ文字列で現れています。 次に、Javascriptで扱えるようにspanタグに「id」と「onclick」イベントハンドラを配置します。 「onclick」で起動する関数は後ほど配置します。 <html> <head> <meta charset="UTF-8"> <title>text and select form</title> </head> <body> <!--「id」と「onclick」を配置します--> <span id="year" onlick="">2021</span>年 <br><br> <!--「id」と「onclick」を配置します--> <span id="line" onlick="">textを書き換えます</span> </body> </html> 次に、Javascriptの本体を書き込むscriptタグを、今回はわかりやすいようにspanタグの直下にそれぞれ配置します。 <html> <head> <meta charset="UTF-8"> <title>text and select form</title> </head> <body> <span id="year" onlick="">2021</span>年 <!--scriptタグを配置します--> <script> </script> <br><br> <span id="line" onlick="">textを書き換えます</span> <!--scriptタグを配置します--> <script> </script> </body> </html> 続いて、最初のscriptタグにspan id="year"用のJavaScriptを書きます。 <span id="year" onlick="">2021</span>年 <script> let spanYearTag=document.getElementById("year"); let optionsArray=[2021,2022,2023];//select要素のoptionsの配列です /*span id="year"のonclickで起動する関数です*/ function toSelect(){ let keepNen=Number(spanYearTag.innerHTML); //span id="year"の内容を(一応数値化して)変数に格納 /*select要素を構築します*/ let selectElement='<select id="year-select" onchange="fromSelect()">';//idとonchangeを書きます for(let i=0; i<optionsArray.length; i++){//optionsArray配列からselect要素のoptionsを書きます selectElement+='<option value="'+optionsArray[i]+'">'+optionsArray[i]+'</option>'; } selectElement+='</select>'; /*span id="year"にselect要素を配置します*/ spanYearTag.innerHTML=selectElement;//ここでselect要素が実体化します /*表示されていた「年」と同じoptionsをselectedにします*/ for(let i=0; i<optionsArray.length; i++){// if(keepNen==optionsArray[i]){ document.getElementById("year-select").options[i].selected=true; break; } } /*span id="year"のonclickハンドラを無効にします*/ spanYearTag.onclick=null;//spanタグ内にselectタグあるので、selectタグへのクリックが上位spanタグに影響しないようにするためです } /*select id="year-select"のonchangeで起動する関数です*/ function fromSelect(){// let selectTag=document.getElementById("year-select"); let selectNen=selectTag.value;//selectで選択されたvalueを格納 /*span id="year"に選択された年を配置します*/ spanYearTag.innerHTML=selectNen;//ここでselect要素と選択された「年」が入れ替わります /*span id="year"にonclickを新たに配置します*/ spanYearTag.onclick=new Function("toSelect()");//nullにしたものを復活です } </script> 上記のJavaSciptのtoSelect関数をspan id="year"のonclickに配置します。 <html> <head> <meta charset="UTF-8"> <title>text and select form</title> </head> <body> <!--toSelect関数をspan id="year"のonclickに設置します--> <span id="year" onlick="toSelect()">2021</span>年 <script> /*--略--*/ </script> <br><br> <span id="line" onlick="">textを書き換えます</span> <script> </script> </body> </html> 次に、二つ目のscriptタグにspan id="line"用のJavaScriptを書きます。 <script> let spanInputTag=document.getElementById("line"); /*span id="line"のonclickで起動する関数です*/ function toText(){ let keepLine=spanInputTag.innerHTML; //span id="line"の内容を変数に格納。 /*text要素を構築します*/ let textElement='<input type="text" id="line-text" value="'+keepLine+'" onblur="fromText()">';//idとonchangeを書きます valueに変更前の文字列を設定します /*span id="line"にtext要素を配置します*/ spanInputTag.innerHTML=textElement;//ここでtext要素が実体化します /*text要素にフォーカスします*/ document.getElementById("line-text").focus(); document.getElementById("line-text").select(); /*span id="line"のonclickハンドラを無効にします*/ spanInputTag.onclick=null;//spanタグ内にselectタグあるので、selectタグへのクリックが上位spanタグに影響しないようにするためです } /*input text id="line-text"のonblurで起動する関数です*/ function fromText(){// let textTag=document.getElementById("line-text"); let getLine=textTag.value;//textで入力されたvalueを格納 /*span id="line"に入力された文字列を配置します*/ spanInputTag.innerHTML=getLine;//ここでtext要素と入力された文字列が入れ替わります /*span id="line"にonclickを新たに配置します*/ spanInputTag.onclick=new Function("toText()");//nullにしたものを復活させます } </script> 上記のJavaSciptのtoText()関数をspan id="line"のonclickに設置します。 <html> <head> <meta charset="UTF-8"> <title>text and select form</title> </head> <body> <span id="year" onlick="toSelect()">2021</span>年 <script> /*--略--*/ </script> <br><br> <!--toText関数をspan id="line"のonclickに設置します--> <span id="line" onlick="toText()">textを書き換えます</span> <script> /*--略--*/ </script> </body> </html> HTMLとJavaScript全体です。 <!doctype html> <html> <head> <meta charset="UTF-8"> <title>text and select form</title> </head> <body> <span id="year" onclick="toSelect()">2021</span>年 <script> let spanYearTag=document.getElementById("year"); let optionsArray=[2021,2022,2023];//select要素のoptionsの配列です /*span id="year"のonclickで起動する関数です*/ function toSelect(){ let keepNen=Number(spanYearTag.innerHTML); //span id="year"の内容を(一応数値化して)変数に格納 /*select要素を構築します*/ let selectElement='<select id="year-select" onchange="fromSelect()">';//idとonchangeを書きます for(let i=0; i<optionsArray.length; i++){//optionsArray配列からselect要素のoptionsを書きます selectElement+='<option value="'+optionsArray[i]+'">'+optionsArray[i]+'</option>'; } selectElement+='</select>'; /*span id="year"にselect要素を配置します*/ spanYearTag.innerHTML=selectElement;//ここでselect要素が出現します /*表示されていた「年」と同じoptionsをselectedにします*/ for(let i=0; i<optionsArray.length; i++){// if(keepNen==optionsArray[i]){ document.getElementById("year-select").options[i].selected=true; break; } } /*span id="year"のonclickハンドラを無効にします*/ spanYearTag.onclick=null;//spanタグ内にselectタグがあるので、selectタグへのクリックが上位のspanタグに影響しないようにしています } /*select id="year-select"のonchangeで起動する関数です*/ function fromSelect(){// let selectTag=document.getElementById("year-select"); let selectNen=selectTag.value;//selectで選択されたvalueを変数に格納 /*span id="year"に選択された年を配置します*/ spanYearTag.innerHTML=selectNen;//ここでselect要素と選択された「年」が入れ替わります /*span id="year"にonclickを新たに配置します*/ spanYearTag.onclick=new Function("toSelect()");//nullにしたものを復活です } </script> <br><br> <span id="line" onclick="toText()">textを書き換えます</span> <script> let spanInputTag=document.getElementById("line"); /*span id="line"のonclickで起動する関数です*/ function toText(){ let keepLine=spanInputTag.innerHTML; //span id="line"の内容を変数に格納。 /*text要素を構築します*/ let textElement='<input type="text" id="line-text" value="'+keepLine+'" onblur="fromText()">';//idとonchangeを書きます valueに変更前の文字列を設定します /*span id="line"にtext要素を配置します*/ spanInputTag.innerHTML=textElement;//ここでtext要素が出現します /*text要素にフォーカスします*/ document.getElementById("line-text").focus(); document.getElementById("line-text").select(); /*span id="line"のonclickハンドラを無効にします*/ spanInputTag.onclick=null;//spanタグ内にselectタグあるので、selectタグへのクリックが上位spanタグに影響しないようにしています } /*input text id="line-text"のonblurで起動する関数です*/ function fromText(){// let textTag=document.getElementById("line-text"); let getLine=textTag.value;//textで入力されたvalueを格納 /*span id="line"に入力された文字列を配置します*/ spanInputTag.innerHTML=getLine;//ここでtext要素と入力された文字列が入れ替わります /*span id="line"にonclickを新たに配置します*/ spanInputTag.onclick=new Function("toText()");//nullにしたものを復活させます } </script> </body> </html> さて、input-text要素に関してですが、 上記のJavaScriptでは、onblurで入力フォームが消えるようにしていますが、 onkeydownでEnterキーをトリガーにすることもできます。 更に上記のinput-text要素は、 value=""で入力フォームが消えてしまった場合の回避措置がありません。 value=""で消えてしまうとクリックすべき文字列がなくなりますので、次はもうクリックできません。 実際に使う場合は、value=""消えようとする時にアラート出すなりの回避措置が必要です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JavaScript] named export と default export の違い

export とか import の構文をES Modulesと呼びます。 ES Modulesを使うために、Deno 環境を用意するとよいです。 Node.js でも package.json 記載すればうまく動きます。 この記事ではどちらでも動かしてみます。 fileA.js export const funcA = () => { console.log('funcA output') }; これが named export 。関数名付きでファイルからexportしています。 fileB.js const funcB = () => { console.log('funcB output') }; export default funcB; これが default export 。名前なしでファイルからexportしています。 使い方はそれぞれこんな感じ。全て同一ディレクトリ内に配置してください。 index1.js import { funcA } from './fileA.js' import funcB from './fileB.js' console.log('index.js'); // index.js funcA(); // funcA output funcB(); // funcB output > deno run index1.js 上記コマンドで、コメントに示している通りの出力が行われます。 これらの名前を変更して利用する場合は次のようにします。 index2.js import { funcA as funcA1 } from './fileA.js' import funcB1 from './fileB.js' console.log('index.js'); // index.js funcA1(); // funcA output funcB1(); // funcB output > deno run index2.js funcAは名前が指定されているので別名で定義するときは as を使います。 funcBの方は、fileBから出力されているってだけで名前なしなので、別名の変数に代入するだけで使えます。 Node.js で ES Modules を動かすためには、.js の拡張子の場合、package.json の記載が必要です。 package.json { "type": "module" } > node .\index1.js > node .\index2.js それぞれのコマンドで動作確認できます。 参考 Node.js で ES Modules 記法を動かしてみる - Neo's World https://neos21.net/blog/2020/09/02-02.html 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Module parse failed: Unexpected string in JSON at position 93 while parsing と表示された時の対処

jsonファイルを手動で弄ると起こるエラー Module parse failed: Unexpected string in JSON at position 93 while parsing '{ "name" : "text", "sex": { "' You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders SyntaxError: Unexpected string in JSON at position 93 while parsing '{... このエラーが起こった時の対処です。 カンマ(,)が無かった { "name" : "abc" "sex": { "male": "true" }  "age": "123" } カンマ(,)が足りていなかっただけでした。 不必要な箇所にカンマがあっても同様のエラーが発生するようです。 簡単ですが、オブジェクトが長くなると見逃しがちです。 jsonエディターで解決 json用のeslintなど検証ツールが入っていれば悩まないはずですが、私はちょうど導入してなくてこのエラーが出ました。 そんな時はWebアプリのjsonエディターを使って解決しましょう。 これに入れると、エラー箇所を教えてくれるので解決です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【jquery+Datatables】行削除と各要素の値チェックに対応した簡単なテーブルを作る

フロントエンドエンジニアではない人が作った簡単な画面です。 だけど、体感的にこういう画面が使いたいときは結構ある気はする(すでにライブラリもあるかもしれないが)。 作ったもの サーバサイドからajax経由でデータを受け取る 名前に日本語などの変な文字列が入っていたり、数式に変な文字(画像の場合は大かっこ[])が入っていると、validation errorを発生させ、テキストフィールドに色を付けて、エラーメッセージをセルの近くに出す IDの付近を押すと「削除」のメニューが近くに出てくる 「削除」を押すと行が消える ※追加については、次の記事で書く予定(若干コードが変わるため) ※削除後イベントとしてajaxを送る必要がある(今回はフロントエンド側メインなので、ajaxでDELETEリクエストを入れる部分は考えない) 解説 各種ポイントだけをピックアップ なお、ASP.NET MVCを使用しているので、jsのプログラムとHTMLのプログラムはIndex.cshtmlにすべて同梱しているものとする ※基本的にはjqueryを使った経験は薄いので、色々ご指摘いただけると助かります。 開発環境 再現性のため、使用したライブラリは全部並べて書く 役割 ライブラリ、フレームワーク バージョン ASP.NET MVC Core 5 jquery v3.5.1 UIライブラリ jqueryui v1.12.1 バリデーションチェック jquery validation plugin v1.17.0 画面整形用CSS bootstrap 4 v4.3.1 データテーブルUI DataTables v1.10.21 ポイント①:menuなどは、DataTablesの添え字に依存する値を突っ込むためにカスタムデータを使う まずはHTMLでの注意。 <!-- ポイント① : data-menuindexとしメニューのクリックした行を確保できるようにする --> <div id="contextmenu" class="text-left" style="display: none;" data-menuindex="0"> <table class="table table-hover table-info table-sm"> <tr onClick="pressMenu('remove')"> <td>削除</td> </tr> </table> </div> メニューは別途HTMLに外部で定義して、それを右クリックしたときにマウスポインタの近くに来るようにしている。 このとき、カスタムデータなどで、DataTablesのクリック位置を覚えておけるようにしておきたい。 今回はたまたまメニューを使っているが、他の機能でもこういうのを使いたいときがあるので注意する ポイント② : 行の識別子としてmeta.rowを使用する DataTablesの定義に注意をしよう 例えば、idは次のようになっている { data: "id", render: function(data, type, instance, meta) { /* ポイント② : 行の識別子としてmeta.rowを使用する */ return '<label name="id_' + meta.row + '" class="js-text-input" data-row="' + meta.row + '">' + data + '</label>'; } }, ajaxでサーバサイドとフロントエンドでやりとりするときは"data"を使うのだが、 "render"を入れることでレンダリングを行うことが出来る。 このとき行番号に対応する値は(私が探した限り)、meta(第4引数)しかなかったので、これを使っている。 引数が長くなるし、データのアクセス権的には少し危うい部分もあるが、こうしてしまえば画面呼び出し時の行を識別子として割り当てることが出来る。 注意として、行の削除は容易だが、任意行の追加を機能として取り入れる場合は、現在表示されている表の行の位置≠meta.rowの値となることに気を付けなくてはいけない。この対処法については別の機会で扱うとして、フロントエンドでこうしたスマートな機能を入れる場合には一工夫がいるのだ。 ポイント③:validatorをinitCompleteの中に入れて、動的にvalidation制約を追加する 一番大事なポイント。 通常、validationをするときはRules制約を使ったコード、例えば $('form').validate({ rules: { "rowName": { required: false, isAlphabet: '' } } }); と書くことが多い。これは簡単なのだが、一つ問題がある。 rulesを「要素の数だけ動的に追加する」機能を必要とするのだ。 従って、このrulesを連想配列で要素の数だけ突っ込む処理が必要なのだが、DataTablesのセル要素を並べる作業は非同期で行われるので、validateを実行するタイミングではDataTablesの中身がまだロードされていない可能性が高い。validateを使うやり方では無理がある。 これを解決するため、DataTablesのinitCompleteを使って、セルロードが完全に行われてからvalidation ruleを決めるようにした。 initComplete: function(settings, ajax_json) { // もしDataTablesの描画が終わったら、 // テキストボックスの色を変えたり // validationルールを加えたりする. for (i = 0; i < ajax_json.length; i++) { /* ポイント③ : validatorをinitCompleteの中に入れて、 動的にvalidation制約を追加する */ $('input[name=rowName_' + i + ']').rules("add", { isAlphabet: true }); $('input[name=expression_' + i + ']').rules("add", { isValidExpression: true }); // メニュー右クリック $('label[name=id_' + i + ']')[0].oncontextmenu = function(e) { $('#contextmenu').css({ 'position': 'absolute', 'left': e.pageX, 'top': e.pageY, 'display': 'block' }); // contextmenuの行の添え字を登録するのを忘れずに $('#contextmenu').data['rowindex'] = this.dataset['row']; // return falseとすることで、 // ブラウザの右クリックを排除できる return false; }; } } 第2引数であるajax_jsonには、ajaxで受け取ったjsonデータが含まれるので、これを各タグの識別子と対応付けると良いと思う。 initCompleteに関する解説(英語)はこちら。 validationを追加するときは、rules("add", ...)を使用する。 動的な制約指定の場合に使用する。 $('input[name=rowName_' + i + ']').rules("add", { isAlphabet: true }); ポイント④ : 値チェックをするaddMethodの中身には、HTMLの各要素を入れても大丈夫 値チェックの処理自体は、動的な制約指定の前に追加しても大丈夫なようです。 ここではjqueryのvalue selectorを使って、色々柔軟に制約を使うことも出来ると思います。 出来れば、色々な列で使えるように一般化できるとなおよし。 $.validator.addMethod('isAlphabet', function(value, element, param) { /* ポイント④ : addMethodの中身には、HTMLの各要素を入れても大丈夫 */ condition = value.match(/^[\w\s]+$/g); if (condition) { $('input[name=' + element.name + ']').removeClass('bg-warning'); return true; } else { $('input[name=' + element.name + ']').addClass('bg-warning'); return false; } }, jQuery.validator.format('このフィールドはアルファベット、数字、アンダーバー(_)、空白のみ指定できます。')); ポイント⑤ : validateは空の状態でも実行しないと、値チェックは行われない。 抜け落ちやすいので注意しましょう。 スクリプト中で、validateを最低1回でも実行しないと、validationは行われません。 空で良いです。 $('form').validate(); ソースコード HTML部分 <!-- ポイント① : data-menuindexとしメニューのクリックした行を確保できるようにする --> <div id="contextmenu" class="text-left" style="display: none;" data-menuindex="0"> <table class="table table-hover table-info table-sm"> <tr onClick="pressMenu('remove')"> <td>削除</td> </tr> </table> </div> <form action="~/AnyData/Index" method="post"> <div class="text-left"> <table class="table table-primary" id="root_table"> <thead> <tr> <th>ID</th> <th>名前</th> <th>式</th> </tr> </thead> </table> </div> <div class="text-left"> <input type="submit" value="送信" class="btn btn-primary"/> </div> </form> JavaScript(Menu部分) function pressMenu(menu_item) { rowindex = $('#contextmenu').data['rowindex']; if (menu_item === "remove") { // ここでは行を単にHTMLから消しているだけだが、 // APIなどの通信なしでサーバサイドとの連携するときは、 // ajaxでDELETEリクエストを送る $('#root_table').DataTable().row( $('label[name=id_' + rowindex + ']').parents("tr")).remove().draw(); } } // メニュー以外押したらメニューを解除 $(document).on('click', function(e) { $('#contextmenu').css('display', 'none'); }); JavaScript(DataTables部分) $(document).ready(function () { datatable = $('#root_table').DataTable({ ajax: { url: "/api/AnyData", dataSrc: "" }, columns: [ { data: "id", render: function(data, type, instance, meta) { /* ポイント② : 行の識別子としてmeta.rowを使用する */ return '<label name="id_' + meta.row + '" class="js-text-input" data-row="' + meta.row + '">' + data + '</label>'; } }, { data: "name", render: function(data, type, instance, meta) { return '<input type="text" name="rowName_' + meta.row + '" class="js-text-input" value="' + data + '"/>'; } }, { data: "expression", render: function(data, type, instance, meta) { return '<input type="text" name="expression_' + meta.row + '" class="js-text-input" value="' + data + '"/>'; } } ], ordering: false, // ソートも禁止する searching: false, // 検索バーを消す info: false, // 情報表示も消す paging: false, // テーブル画面のPreviousやNextは消す initComplete: function(settings, ajax_json) { // もしDataTablesの描画が終わったら、 // テキストボックスの色を変えたり // validationルールを加えたりする. for (i = 0; i < ajax_json.length; i++) { /* ポイント③ : validatorをinitCompleteの中に入れて、 動的にvalidation制約を追加する */ $('input[name=rowName_' + i + ']').rules("add", { isAlphabet: true }); $('input[name=expression_' + i + ']').rules("add", { isValidExpression: true }); // メニュー右クリック $('label[name=id_' + i + ']')[0].oncontextmenu = function(e) { $('#contextmenu').css({ 'position': 'absolute', 'left': e.pageX, 'top': e.pageY, 'display': 'block' }); // contextmenuの行の添え字を登録するのを忘れずに $('#contextmenu').data['rowindex'] = this.dataset['row']; // return falseとすることで、 // ブラウザの右クリックを排除できる return false; }; } } }); // DataTables Validationの定義 // 名前判定 $.validator.addMethod('isAlphabet', function(value, element, param) { /* ポイント④ : addMethodの中身には、HTMLの各要素を入れても大丈夫 */ condition = value.match(/^[\w\s]+$/g); if (condition) { $('input[name=' + element.name + ']').removeClass('bg-warning'); return true; } else { $('input[name=' + element.name + ']').addClass('bg-warning'); return false; } }, jQuery.validator.format('このフィールドはアルファベット、数字、アンダーバー(_)、空白のみ指定できます。')); // 式判定 $.validator.addMethod('isValidExpression', function (value, element, param) { condition = value.match(/^[\s|\$\d+|\d|\=\+\-\*\/]+$/g); if (condition) { $('input[name=' + element.name + ']').removeClass('bg-warning'); return true; } else { $('input[name=' + element.name + ']').addClass('bg-warning'); return false; } }, jQuery.validator.format('正しい式ではありません')); /* ポイント⑤ : validateは空の状態でも実行しないと、値チェックは行われない。 */ // 一度空のvalidateを入れないと // フォーム全体ではvalidationしてくれない $('form').validate(); }); 感想 jqueryもdatatablesも大変だが、扱えたら素晴らしいUIが作れることがよくわかる。 私はアプリケーション系の開発経験がWeb系の開発経験よりも多いのだが、 こうしたDataTablesを使った機能拡張は結構アプリケーションだとやりにくいことが多い。 上手いことWebの強みを生かすときに、あると便利だなぁと思いました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Jexcel]Excelで開けるUTF-8ファイルを作る

※いわゆる「//なぜか動く」もので、個人の覚え書きです。 いまはJexcelでなく「Jspreadsheet」に改名したようですね。 設置方法については他の方の記事をご参照ください。 CSVダウンロードすると当然のごとくUTF-8で落ちてくる CSVビューアーを使ってくれればいいのですが、だいたいの企業の人間はCSVファイルを開くのにExcelを使います。 Excelで直接CSVファイルを開くと、文字コードがSJISでないと日本語が文字化けします。 ダウンロード文字コードを変える jexcel.jsの5926行目あたりにある以下の記述を変える jexcel.js var blob = new Blob(["\uFEFF"+data], {type: 'text/csv;charset=utf-8;'}); ↓ jexcel.js var blob = new Blob(["\uFEFF"+data], {type: 'text/csv;charset=sjis;'}); これだけで「Excelでそのまま開けるがテキストエディタでUTF-8と判定されるCSVファイル」が落ちるようになります。 「\uFEFF」はBOMです。 SJISでは本来不要ですが、これ外すとExcelで文字化けします。 いまのところCSVビューア勢からもExcel勢からも特に何も言われていませんが、バイナリレベルで操作すると何か不具合を起こすかもしれないため、自己責任で。  
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt jsonデータからバインディングした値で画像を表示させる

はじめに タイトルの日本語があってるのかわかりませんが、やりたいのはこういう事?です。ソースを見てわかってください。。。 assets/js/data.js export const resultsData = [ { id: "00", text: "熱中する人", img: "charmander.png" }, { id: "01", text: "達成する人", img: "caterpie.png" }, { id: "02", text: "挑戦する人", img: "charmeleon.png" }, ] ここで定義したデータに画像が入ってます。 それを <h2>{{ result.text }}</h2> <script> import { resultsData } from "~/assets/js/data.js"; computed: { result() { return resultsData.find( (result: { id: string }) => result.id == (this as any).answerKey ); }, }, </script> みたいな感じで画像を取得するにはどうしたら良いか? というお話です! バッククオートが重要 <h2>{{ result.text }}</h2> <div class="mt-3"> <img :src="require(`@/assets/images/type/${result.img}`)" class="contentImg" /> </div> <script> import { resultsData } from "~/assets/js/data.js"; computed: { result() { return resultsData.find( (result: { id: string }) => result.id == (this as any).answerKey ); }, }, </script> この部分がなんて書いたら良いか分からず、、色々と試行錯誤した結果このようになりました! <img :src="require(`@/assets/images/type/${result.img}`)" class="contentImg" /> requireを""で囲って、()内のpathの部分はバッククオートで囲うのが正解なようです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Apollo v3のページネーションについて

公式ドキュメント 何についての記事か? Aoikki v3でのページネーション(特に、カーソルページネーション)についての記事になります。 主に、公式ドキュメントだとわかりにくかった部分の補足を書きます。 基本的な部分については公式ドキュメントを参考にしてください。 v3と前バージョンの大きな違いは何か? updateQueryがdeprecatedになり、Cache APIにマージの処理を書くようになったことです。 より効率的にキャッシュを使うための施策のようです。 どのように書くか?(具体例) クエリ実行の時、今までは updateQuery でマージの処理を書いていましたが、その記述は不要になります。 代わりに、CacheAPIでマージ処理を書きます。 export const useFeedsQuery = (variables: FeedsQueryVariables) => { // クエリの実行 const { data, fetchMore, loading } = useQuery(FEED_QUERY, { variables: { id: "XXX" collectionInput: { limit: 10, cursor: "YYY", } },  fetchPolicy: 'cache-and-network',  nextFetchPolicy: 'cache-first', notifyOnNetworkStatusChange: true });  // 次ページ取得関数 const handleMore = useCallback(() => { fetchMore({ variables: { collectionInput: { ...variables.collectionInput, cursor: data?.feeds?.pageInfo.endCursor, }, }, }); }, [variables.collectionInput, data?.feeds?.pageInfo.endCursor, fetchMore]); return { feeds: data?.feeds?.edges || [], hasMore: data?.feeds?.pageInfo.hasNextPage || false, handleMore, loading, }; }; // CacheAPIのマージ部分 type FeedQueryResult = Pick<FeedQuery['feed'], 'pageInfo' | 'edges'>; type QueryResultItem = { id: string }; const cache = new InMemoryCache({ typePolicies: { Query: { fields: { feeds: { keyArgs: ['id'], merge( existing: FeedQueryResult, incoming: FeedQueryResult, { args, readField } ): FeedQueryResult { const merged = { pageInfo: incoming.pageInfo, edges: [...(existing?.edges ?? [])], }; let offset = offsetFromCursor(merged.edges, args?.collectionInput?.cursor, readField); if (offset < 0) offset = merged.edges.length; merged.edges = [...merged.edges.slice(0, offset), ...incoming?.edges]; return merged; }, } } } } }) const offsetFromCursor = (items: QueryResultItem[], cursor: string, readField: ReadFieldFunction): number => { if (!cursor) return 0; for (let i = items.length - 1; i >= 0; --i) { const item = items[i]; const id = readField('id', item); if (id === cursor) { return i + 1; } } return -1; }; 上記の書き方について以降で解説します。 ここで解説していない基本的な部分などについては、公式ドキュメントを参考にしてください。 具体例の解説(useQuery) fetchPolicy useQueryを実行したとき、データをどのように返すかを制御するものです。 次の種類があります。 fetchPolicy 説明 cache-first(デフォルトの動作) キャッシュがあればそれを返す、なければサーバーから取ってくる。 cache-and-network キャッシュがあればそれを返すが、同時にサーバーから取ってきてキャッシュを更新する。 network-only キャッシュは利用せず、サーバーから取ってきたものを返す(この場合もキャッシュは更新される)。 no-cache network-onlyのキャッシュを更新しない版。 cache-only cache-firstのサーバーからとってこない版(キャッシュになければ、エラーになる)。 standby nextFetchPolicy 指定できるものはfetchPolicyと同じです。 useQueryを実行した後、ネットワーク状態の更新などでコンポーネントも更新されることがあり、useQueryも複数回実行される可能性がありますが、この時の振る舞いを決めるものとなります(おそらく)。 何も設定しないと、fetchPolicyと同じ値になります。 notifyOnNetworkStatusChange これは通信状況の監視を行うかのフラグで、trueにしないとloadingは常にfalseになります。 具体例の解説(fetchMore) variables ここでのvariablesは変更になったものだけを書いてやれば良いです。 useQueryに渡したものとマージされます。 ただし、マージされるのは直下のみで、ネストしている場合は自分でマージする必要があります。 具体例の解説(CacheAPI) typePolicies GraphQLクエリタイプの_typenameの値がtype名やfield名になります。 例えば上記の具体例だと、以下のようなクエリタイプがある想定です。 // GraphQLクエリタイプ export type FeedsQuery = { __typename?: 'Query' } & { feeds: { __typename?: 'FeedPageInfo' } & { pageInfo: { __typename?: 'CollectionPageInfo' } & Pick< Types.CollectionPageInfo, 'startCursor' | 'endCursor' | 'hasNextPage' >; edges: Array<{ __typename?: 'Feed' } & FeedsFragment>; }; }; 上記の具体例は次のように分割することも可能です(この分割の仕方はあまり良くないですが)。 詳細は、こちらを確認してください。 export const newCache = (): InMemoryCache => { return new InMemoryCache({ typePolicies: { Query: { fields: { feeds: { keyArgs: false, merge: true, }, }, }, TopicPageInfo: { fields: { pageInfo: { merge(existing: CollectionInput, incoming: CollectionInput) { ... }, }, edges: { merge(existing: Array<FeedFragment>, incoming: Array<FeedFragment>) { ... }, }, }, }, } }); }; 分割する場合は単一要素(サービス内でユニークな要素)のみにした方が良さそうです。 上の例のように複数要素が入るようなfieldを分割すると、予期しないところからキャッシュが更新されてしまう可能性があります。 keyArgs args内のどのフィールド単位でキャッシュを分けるかを指定できます(複数指定可能)。 これを使わず、merge内でargsのフィールドを元にしたkeyを作りmapで管理する方法もあります(この場合、取り出す時にクエリのレスポンスの形にする必要があるのでreadの処理を書く必要があります)。 ただ、責任を分けるためにもkeyArgsを使うのが無難です。 // キャッシュを自分で分割する方法の例(具体例とは対応していません) feeds: { read(existing = {}, { args: { groupId, offset, limit }}) { return existing[groupId] && existing[groupId].slice(offset, offset + limit); }, merge(existing = {}, incoming, { args: { groupId }}) { const merged = existing[groupId] ? existing[groupId].slice(0) : []; for (let i = 0; i < incoming.length; ++i) { merged[offset + i] = incoming[i]; } existing[groupId] = merged; return existing; }, }, merge キャッシュのデータと新しくとってきたデータをどのようにマージするか指定します。 第三引数にどういう条件でクエリが呼ばれたかを示すargs、readFieldやobjectMergeといったヘルパー関数が含まれます。 次のように書くだけだとうまくいかないケースも多いので気をつけてください。 return [...existing, ...incoming]; read 具体例のように何も指定がない時はmergeの返り値をそのまま返します。 取り出すときに何か手を加えたいときはこれに処理を書きます(例えば、一部だけ返したいときなど)。 トラブルシューティング loadingを使った処理がうまく動かない [原因: notifyOnNetworkStatusChange: trueの設定がない] notifyOnNetworkStatusChange: true が useQueryのオプションに設定されていない可能性があります。 次ページを取得しようとすると無限ループになる [原因: nextFetchPolicyがnetwork系になっている] nextFetchPolicyがnetwork系になっていると、例えば次のようなことが発生します。 前提: 画面下に来ると次ページを取得する 1. fetchMoreを実行して次のページを取得する 2. キャッシュのリストが更新されるタイミングで再レンダリング、useQueryも実行されるが、その時もう一度最初のリクエスト(cursorなし)が飛ぶ 3. 1のレスポンスでリストを上書きするようになっている場合、画面下の状態になるので、再び1を実行する。 対応: nextFetchPolicyをcache-firstにする。 こうすると、loading状態が切り替わったタイミングでも、キャッシュ(最初のリクエスト実行後、結果が追加されたリスト)が返るようになり、画面下にならなくなるので無限ループは解消されます。 fetchPolicyもcache-firstにすると、リスト内容が更新されないので注意してください(fetchPolicyは、network-only か cache-and-network が良いと思います)。 mergeの第三引数のargsが取れない [原因: argsはuseQueryで直接実行したクエリタイプに対応するtypePolicyのmergeにしか値は入らない] fieldを分割していた場合、そちらのmerge(例えば、前述のTopicPageInfo内のmerge)のargsは常にnullになります。 データが重複して取れてしまう [原因: fetchPolicyにnetwork系を指定し、mergeでいつもexistingにincomingを追加している] fetchPolicyにnetwork系を指定していると、コンポーネントのマウント(ページ遷移時など)の際、キャッシュがあってもuseQueryに渡した条件でリクエストが飛ぶので、新しく取ってくるデータは最初のページになります。 mergeの処理がexistingにincomingを追加する、というような処理になっていると、最初のページが二重でキャッシュに乗ってしまいます。 対応: argsのcursor(idなど)と同じ値を持つ要素があった場合、それ以降の要素は一旦消してからincomingを追加する。 ページ遷移をしてもリストが更新されない [原因1: mergeでいつもexistingにincomingを追加するようになっている] こうすると、古いデータが上書きされずに残ってしまう。 対応: 特定の条件下ではexistingをincomingで上書きする。 [原因2: fetchPolicyがcache-firstになっている] cache-firstだとキャッシュがあればいつもそれを使うので、新しいデータがいつまで経っても取得されない。 対応: fetchPolicyをnetwork系にして、必要であればnextFetchPolicyをcache-firstにする。 Q&A useQueryとfetchMoreの違いは? useQueryはfetchPolicyに応じて値を返すのに対して、fetchMoreはリクエストを送り、その結果でキャッシュを更新する。 fetchPolicyとnextFetchPolicyの違いは? fetchPolicyはコンポーネントがマウントされて最初のuseQuery呼び出しで有効なのに対して、nextFetchPolicyは再レンダリングの時などにuseQueryが呼び出される時に有効となる(おそらく)。 mergeの第三引数のargsとvariablesの違いは? argsには実行したGraphQLクエリの引数が入ってきます。 フィールド名もクエリの引数名と同じになるので、variablesのフィールド名とは異なることに注意してください。 query Feeds( $feedsCollectionInput: CollectionInput! $groupId: String! ) { feeds(id: $groupId, collectionInput: $feedsCollectionInput) { pageInfo { startCursor endCursor hasNextPage } edges { ...Feed } } } 例えば、上のようなクエリだとargsは下のようになります。 // args { id: "XXX", collectionInput: { ... } } variablesは以下のようになるので、違いがわかると思います。 { feedsCollectionInput: { ... }, otherCollectionInput: { ... }, groupId: "XXX" }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「Denoにはnpmがない」の誤解

Node.jsとDenoを比較する際の特徴として、「npmがない」というのがよく言及されています。 これ自体は正しいのですが、まるでパッケージ管理が貧弱かのように聞こえてしまい、誤解を生んでいるので、改めて整理していきたいと思います。 Denoのパッケージ管理についての誤解 誤解①:npmのパッケージが使えない DenoはランタイムとしてNode.jsとの互換性を切ったものの、Node.js向けのコードをブラウザ向けのコードに変換するツールは沢山あります。Denoはブラウザ向けのコードがそのまま動くので、npmパッケージをDenoで利用することが可能です。 現時点では以下のレジストリを経由してDenoでnpmパッケージを使用できることが知られています。 https://esm.sh/ https://unpkg.com/ https://www.skypack.dev/ https://jspm.org/ ただし注意点として、Node.jsの標準ライブラリはDenoの標準ライブラリでポリフィルされているのですが、このポリフィルの実装が途中です。 なので、importしたいnpmパッケージが未実装の標準ライブラリに依存していると、エラーが出る可能性があります。 ちなみに、「ファイル中にrequire()が含まれていると使えない」というのは古い情報です。 誤解②:実行のたびに外部パッケージのURLにアクセスする 外部URLからのimportを用いた場合、初回実行時のみモジュールのURLにアクセスし、2回目以降の実行ではローカルのキャッシュを使用します。 キャッシュに使うディレクトリは環境変数DENO_DIRで制御することが出来ます。 誤解③:パッケージマネージャが無い Node.jsの場合、npmやyarnといったパッケージマネージャが外部コマンドとして存在します。 Denoの場合はどうでしょうか。 @ry: URLインポートの考え方は、Denoが独自のパッケージマネージャーとして機能するというものです。補助的なツールの必要性を明確に避けたいと思います。 https://github.com/denoland/deno/issues/47#issuecomment-395405713 上記コメントにある通り、Deno自体がパッケージマネージャの役割を果たします。 依存関係の解決、依存関係のダウンロード、依存関係の一覧表示など、Node.jsでは外部コマンドとして存在したものがDeno本体に組み込まれています。 パッケージマネージャが無いというよりは、パッケージマネージャの外部コマンドが無いと言ったほうが正しいと思います。 余談ですが、Denoはサードパーティ製ツールの必要性を極力無くすように作られています。フォーマッター、リンター、分散サーバーレスコンピューティング(deno deploy)等がDenoから提供されています。 今まではサードパーティ製ツールの数でエコシステムの成熟度を判断しているような所がありましたが、Denoにおいては公式が提供するものだけで一通り揃うようになっています。 誤解④:import文に必ずURLを使用する必要がある Node.jsとブラウザでは、import文でモジュールを指定する方法が異なります。 ブラウザ // 「モジュールが配信されているURL」を指定する import _ from "https://esm.sh/lodash/"; Node.js // 「モジュール名」を指定する import _ from 'lodash' 本来、Node.jsとブラウザでは(それぞれに固有のAPIを使用しなければ)全く同じコードが利用できるはずです。それなのに、import文の書き方が違うだけで、バンドラ等を利用して実行前に変換作業を行う必要が出てきます。面倒です。 Denoでは、URLを利用したimportが導入されたので、ブラウザと同形のコードが利用できます。バンドラ等の変換プロセスを導入する必要はありません。 ブラウザ&Deno import _ from "https://esm.sh/lodash/"; また、Node.jsとブラウザのimportを近づけるための手段として、import-mapも導入されました。これはブラウザとDenoの両方で利用可能です。 import-map.json { "imports": { "lodash": "https://esm.sh/lodash/" } } import-mapを指定したブラウザ&Deno import _ from 'lodash' import-mapを利用すると、Node.jsのようなスタイルでのimportも可能です。 つまり、 ブラウザ互換のimport文が書ける 必要に応じてNode.jsスタイルのimport文も書ける ということです。 誤解⑤:バージョン管理が面倒 Denoでは、モジュールのバージョン情報もURLで指定するようになっています。 import { assertEquals } from "https://deno.land/std@0.106.0/testing/asserts.ts"; ^^^^^^^^ モジュールのバージョンを上げたい時はどうすればいいでしょうか。ソースコード中のimportをすべて書き換えるのは骨が折れる作業です。 JavaScriptにはexport * from ...という構文が用意されています。これを使って、プロジェクト内のimport文を一箇所にまとめることが出来ます。 deps.ts export * from "https://deno.land/std@0.106.0/testing/asserts.ts"; mod.ts import { assertEquals } from "./deps.ts" export * from ...構文を使って外部モジュールのimportを一箇所に集約することが、公式ドキュメントでも推奨されています。 また、先ほど紹介したimport-mapも、バージョンの一括指定に利用することが出来ます。 誤解⑥:ロックファイルが無い Node.jsにはpackage.jsonのほかにpackage-lock.jsonがありました。Denoにはpackage.jsonが無いのでロックファイルも無いのでは?と思われるかもしれませんが、実行時に--lockフラグを指定してロックファイルを利用できます。 https://deno.land/manual/linking_to_external_code/integrity_checking#caching-and-lock-files npmにあってDenoに無いもの 「Denoにはnpmがない」の言葉通り、npmだけに存在する機能もあります。 package.json Denoにはpackage.jsonがありません。 例えばURLからimportする時に、URLのパスを遡ってpackage.jsonを読みに行くというのはパフォーマンスが悪いですし、webと非互換になってしまいます。 では、バージョン情報などのメタデータはどうやって指定するのかというと、gitのtagが推奨されているようです。 ちなみに、tsconfig.jsonやimport-map.json、Deno.json(設定ファイル)は存在しています。 これらはどれもプログラムのエントリポイントで指定されるもので、パッケージのメタデータを指定するために利用するものではありません。 (Deno.jsonはv1.14以降のみ) node_modules Denoにはnode_modulesがありません。 npmは、「プロジェクト内のnode_modulesフォルダ」にモジュールをダウンロードします。 この挙動により、プロジェクト毎にパッケージがダウンロードされるため、node_modulesフォルダの肥大化が問題になっていました。(インストール先をパッケージ間で共有するように変更したpnpmも存在します。) 一方Denoは、「ローカルのキャッシュフォルダ」にモジュールをダウンロードします。これはpnpmと同じ方式です。ダウンロードされたモジュールは他のプロジェクトと共有されます。 中央集権的なモジュールレジストリ Denoには中央集権的なモジュールレジストリがありません。 Node.jsの場合、npmレジストリが唯一のレジストリです。npmではサーバーが落ちたり、悪意のあるパッケージによってトークンが流出したりするなどの障害が発生していますが、それでもnpmを使い続けなければなりません。また、一度公開されたモジュールが削除されるなどの問題も発生しています。 対してDenoは、任意のレジストリを使用することができます。ユーザーは https://deno.land/x や https://esm.sh や https://www.skypack.dev/ などの中から好きなレジストリを選ぶことが出来ます。レジストリ間で資本主義的な競争原理が働くことで、エコシステム全体の耐障害性や安全性、永続性が向上することが狙いのようです。 まとめ Denoにあるもの パッケージマネージャ 分散型のモジュールレジストリ ブラウザ互換のimport npmから利用可能な豊富なモジュール群 URLの代わりにモジュール名を使用してimportする方法 ロックファイル Denoにないもの 中央集権的なモジュールレジストリ モジュール管理のために必要な外部コマンド package.json node_modules 感想 Denoのモジュール管理方式に対しては、「import文にURLを使うの不便じゃないの?」が一番大きな疑問じゃないかと思います。 結論から言うと、とても便利です。私はローカルで雑用作業に使っている言語(実行環境)をPythonからDenoに切り替えましたが、「ローカルにどのバージョンのパッケージがインストールされているか」を考えなくて済むというのが大きいです。他にも、「URLをctrl-clickすればドキュメントに飛べる」というのも便利です。 最新のエディタ拡張機能を使っていれば未知のURLも自動で補完されるので、入力の面倒さは大分改善されています。 逆に不便に感じた事としては、 「node_modules内のファイルを弄って挙動を確認する」のような事が面倒 npm scriptに相当するものが無い というのがあります。しかし、普通の使い方をする上では問題ないと思っています。 参考 たぶんパッケージ管理ツールが必要です https://github.com/denoland/deno/issues/47 モジュールのバージョン管理、依存関係の管理など https://github.com/denoland/deno/issues/4574 パッケージの提案 https://github.com/denoland/deno/issues/288 URLから直接モジュールをロードするのはとてもかわいいです(※) https://github.com/denoland/deno/issues/195 ※ライアン・ダール(作者)が「Node.jsに関して後悔している10の事」という講演で、index.jsのモジュール解決について index.htmlみたいでかわいいと思った。しかし、モジュール解決が不必要に複雑になってしまった。 と語った事のオマージュ?ということらしい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kintone 日付 to 日付の重複チェック

2つの日付の期間でレコード重複を確認する。 期間内に重なる日付がある時にエラーを検知して警告したい。 この記事はコメントを頂き、有効な内容は記事最下部と致しました。 記事内容 例) 図書館の本の借用を例として考える。 Aさんが9/1~7にとある本を既に借りていて、 同じ本を借りようとした場合にエラーを起こしたい。 レコード1が既に登録されていて、レコード2を登録されるとき。 レコード1 借用者:A 借用本:kintone認定 アソシエイト試験対策テキスト 本番号:2203456903 開始日:9/1 終了日:9/7 新規レコード2 借用者:A 借用本:kintone認定 アソシエイト試験対策テキスト 本番号:2203456903 開始日:9/3 終了日:9/9 レコード1 レコード2を追加しようとするとエラー 解決策 結論、JavaScriptでの日付計算はほとんど利用しない。 テーブルを用意し、2フィールドの日付間の日数分、日付データを格納する。 重複チェックでレコードを参照する際にテーブルの日付データをクエリに指定する。 尚、js初心者向けに全記載のコードで説明していきます。 フィールド フィールドコード フィールドタイプ 開始日 日付フィールド 終了日 日付フィールド 本番号 文字列一行(数値でもok) 借用者 文字列一行 テーブル テーブル 日付 テーブル --> 日付フィールド 借用本 文字列一行(今回は不使用) テーブルへ日付格納 日付フィールドのみのテーブルを作成し、 保存イベントで日付間の全ての日付を格納。 ソースコード Ver0.1 (() => { 'use strict'; //与えられた日付型から文字列の日付を返す関数(yyyy-MM-dd)0埋め日付 const getDateString = (date) => { return `${date.getFullYear()}-${("0" + (date.getMonth() + 1)).slice(-2)}-${("0" + date.getDate()).slice(-2)}`; } //二つの日付フィールドの差を計算して日数で返す関数 //1を足す理由はその日を含める計算のため9/1~9/7は合計7日間 //ミリ秒単位の差分計算の結果 / 1000ms * 60sec * 60 min * 24hour = 差分日数 const getDifference = (dateFrom, dateTo) => { return 1 + Number((new Date(dateTo) - new Date(dateFrom)) / (1000 * 60 * 60 * 24)); } kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], (event) => { const record = event.record; //入力必須フィールド空チェック const mandatoryFields = [ '開始日', '終了日', '本番号', '借用者' ]; //必須チェック const result = mandatoryFields.reduce((acc, field) => { record[field].error = null; if (!record[field].value) { record[field].error = '必須です'; acc.push(field); } return acc; }, []); //一つでも空であればエラー文と共にreturn if (result.length > 0) return event; const totalDays = getDifference(record['開始日'].value, record['終了日'].value); //テーブル初期化 record['テーブル'].value = []; let date = new Date(record['開始日'].value); let i = 0; //合計日数分繰り返し do { //テーブルに日付を格納 record['テーブル'].value.push({ value: { "日付": { type: "DATE", value: getDateString(date) } } }); //日付インクリメント(繰り上げ) date.setDate(date.getDate() + 1); //繰り返し用のインクリメント(繰り上げ) i++; } while (i < totalDays); return event; }); })(); 重複チェック kintone.apiで条件付きでレコードを取得する。 条件: 借用者がAで2203456903の本番号の本を9/1、9/2、9/3. . . 9/7に借りているレコードが 編集中のレコードを除き、存在するか。 kintone.api、GETの際クエリ指定が今回の肝になる。 上記の条件をクエリ記法でどのように表すか クエリをどうするか and と or の使い分け。 最初の例の場合で考えるとすると、 借用者 = "A" and 本番号 = "2203456903" and 日付 = "2021-09-01" or 借用者 = "A" and 本番号 = "2203456903" and 日付 = "2021-09-02" or ......... 借用者 = "A" and 本番号 = "2203456903" and 日付 = "2021-09-07" と日付単位で絞り込んでいく。 ダメな例 借用者 = "A" and 本番号 = "2203456903" and 日付 = "2021-09-01" or 日付 = "2021-09-02 or ...." こうなると、最初の9/1日だけ、借用者と本番号の指定があり、 9/2以降は日付が9/2のすべてのレコードを参照することになる。 ソースコード Ver1.0 (() => { 'use strict'; //与えられた日付型から文字列の日付を返す関数(yyyy-MM-dd)0埋め日付 const getDateString = (date) => { return `${date.getFullYear()}-${("0" + (date.getMonth() + 1)).slice(-2)}-${("0" + date.getDate()).slice(-2)}`; } //二つの日付フィールドの差を計算して日数で返す //1を足す理由はその日を含める計算のため9/1~9/7は合計7日間 //ミリ秒単位の差分計算の結果 / 1000ms * 60sec * 60 min * 24hour = 差分日数 const getDifference = (dateFrom, dateTo) => { return 1 + Number((new Date(dateTo) - new Date(dateFrom)) / (1000 * 60 * 60 * 24)); } kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], (event) => { const record = event.record; //入力必須フィールド空チェック const mandatoryFields = [ '開始日', '終了日', '本番号', '借用者' ]; //必須チェック const result = mandatoryFields.reduce((acc, field) => { record[field].error = null; if (!record[field].value) { record[field].error = '必須です'; acc.push(field); } return acc; }, []); //一つでも空であればエラー文と共にreturn if (result.length > 0) return event; const totalDays = getDifference(record['開始日'].value, record['終了日'].value); //テーブル初期化 record['テーブル'].value = []; let date = new Date(record['開始日'].value); //****************************以下追記******************************// let query = ``; //****************************以上追記******************************// let i = 0; //合計日数分繰り返し do { //テーブルに日付を格納 record['テーブル'].value.push({ value: { "日付": { type: "DATE", value: getDateString(date) } } }); //****************************以下追記******************************// //基本のクエリ let basicQuery = `借用者 = "${record['借用者'].value}" and 本番号 = "${record['本番号'].value}" and 日付 in ("${getDateString(date)}")`; //クエリ記法繰り返し二回目以降は or を追記(三項演算子を使用) query += i === 0 ? basicQuery : ` or ` + basicQuery; //新規レコードの場合と編集レコードの場合で自レコードを含むか含まないかクエリ分岐 query += event.recordId ? ` and $id != "${event.recordId}"` : ''; //****************************以上追記******************************// //日付インクリメント(繰り上げ) date.setDate(date.getDate() + 1); //繰り返し用のインクリメント(繰り上げ) i++; } while (i < totalDays); //****************************以下追記******************************// //最後にソート文 query += ' order by レコード番号 desc limit 100 offset 0'; const param = { app: event.appId, query: query } return kintone.api(kintone.api.url('/k/v1/records', true), 'GET', param).then((resp) => { if (resp.records.length > 0) { event.error = '日付の範囲内に、既に記録された日付が含まれています。'; record['開始日'].error = '確認'; record['終了日'].error = '確認'; } return event; }).catch((error) => { console.log(error); event.error = '既存レコードの取得に失敗しました。'; return event; }); //****************************以上追記******************************// //return event; 削除 }); })(); 今回の肝 テーブルに指定期間の日付をすべて格納 絞り込み条件に一致させるため、9/1、9/7の間の日付を準備すると、 標準機能の絞り込みでも使用できるようになるため、便利。 テーブル内の日付を含めた条件で重複チェック 標準機能の絞り込みも使用できるということは、 apiで該当レコードを取得することも容易ですね。 jsで日付計算は複雑になりがちなので避けてみました。 逆にテーブルの設置無し、jsのみで対応が難しかったから、が本音なので、 方法があれば教えてほしいです。。 フィールドコード書き換えれば使えますというやつ Ver1.0のコメントアウトを取っただけです。 JS (() => { 'use strict'; const getDateString = (date) => { return `${date.getFullYear()}-${("0" + (date.getMonth() + 1)).slice(-2)}-${("0" + date.getDate()).slice(-2)}`; } const getDifference = (dateFrom, dateTo) => { return 1 + Number((new Date(dateTo) - new Date(dateFrom)) / (1000 * 60 * 60 * 24)); } kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], (event) => { const record = event.record; const mandatoryFields = [ '開始日', '終了日', '本番号', '借用者' ]; const result = mandatoryFields.reduce((acc, field) => { record[field].error = null; if (!record[field].value) { record[field].error = '必須です'; acc.push(field); } return acc; }, []); if (result.length > 0) return event; const totalDays = getDifference(record['開始日'].value, record['終了日'].value); record['テーブル'].value = []; let date = new Date(record['開始日'].value); let query = ``; let i = 0; do { record['テーブル'].value.push({ value: { "日付": { type: "DATE", value: getDateString(date) } } }); let basicQuery = `借用者 = "${record['借用者'].value}" and 本番号 = "${record['本番号'].value}" and 日付 in ("${getDateString(date)}")`; query += i === 0 ? basicQuery : ` or ` + basicQuery; query += event.recordId ? ` and $id != "${event.recordId}"` : ''; date.setDate(date.getDate() + 1); i++; } while (i < totalDays); query += ' order by レコード番号 desc limit 100 offset 0'; const param = { app: event.appId, query: query } return kintone.api(kintone.api.url('/k/v1/records', true), 'GET', param).then((resp) => { if (resp.records.length > 0) { event.error = '日付の範囲内に、既に記録された日付が含まれています。'; record['開始日'].error = '確認'; record['終了日'].error = '確認'; } return event; }).catch((error) => { console.log(error); event.error = '既存レコードの取得に失敗しました。'; return event; }); }); })(); コメントで一蹴していただきました。(こちらがシンプルです。) @rgn212さん こんな感じでは駄目でしょうか? // 既存貸し出し期間 const existing = { start: 20210901, end : 20210907, }; // 申請期間 const request = { start: 20210903, end : 20210909, }; if(request.end >= existing.start && existing.end >= request.start) { console.log('重複しています'); } else { console.log('重複していません'); } 応用して、 開始日が新規終了日より小さく 終了日が新規開始日より大きいものを取得。以上。 let query = ``; const includeOwn = event.recordId ? ` and $id != "${event.recordId}"` : ''; query = `借用者 = "${record['借用者'].value}" and 本番号 = "${record['本番号'].value}" and 終了日 >= "${record['開始日'].value}" and 開始日 <= "${record['終了日'].value}"${includeOwn}` query += ' order by レコード番号 desc limit 100 offset 0'; (() => { 'use strict'; kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], (event) => { const record = event.record; const mandatoryFields = [ '開始日', '終了日', '本番号', '借用者' ]; const result = mandatoryFields.reduce((acc, field) => { record[field].error = null; if (!record[field].value) { record[field].error = '必須です'; acc.push(field); } return acc; }, []); if (result.length > 0) return event; let query = ``; const includeOwn = event.recordId ? ` and $id != "${event.recordId}"` : ''; query = `借用者 = "${record['借用者'].value}" and 本番号 = "${record['本番号'].value}" and 終了日 >= "${record['開始日'].value}" and 開始日 <= "${record['終了日'].value}"${includeOwn}` query += ' order by レコード番号 desc limit 100 offset 0'; const param = { app: event.appId, query: query }; return kintone.api(kintone.api.url('/k/v1/records', true), 'GET', param).then((resp) => { console.log(resp); if (resp.records.length > 0) { event.error = '日付の範囲内に、既に記録された日付が含まれています。'; record['開始日'].error = '確認'; record['終了日'].error = '確認'; } return event; }).catch((error) => { console.log(error); event.error = '既存レコードの取得に失敗しました。'; return event; }); }); })();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

p5-matter を使って p5.js での物理演算エンジン(Matter.js)の利用を簡単化する【活用編1】

以下の記事で扱ってきた、 p5.js で物理演算エンジン「Matter.js」を利用する話の続きです。 p5.js と物理演算エンジン「Matter.js」の組み合わせをお試し - Qiita p5-matter を使って p5.js での物理演算エンジン(Matter.js)の利用を簡単化する【概要編】 - Qiita 1つ目の記事では、p5.js と Matter.js をそのまま組み合わせて、2つ目の記事ではそれら 2つの併用を簡単化してくれる p5-matter について軽く触れました。 今回の記事では p5-matter の情報をさらに見ていきつつ、1つ目の記事で掲載していたプログラムを p5-matter を使ったものに書きかえていきます。 ちなみに、できあがりはこのようなものになります。 #p5js と Matter.js を直接組み合わせて、物理演算エンジンに入門をしていた際のサンプル、それを p5-matter を使って書き直してみた!p5-matter を使うことでプログラムがかなり短くなりました。あと、全く一緒なのもつまらない気がしたので、摩擦係数等のパラメータを一部いじってみたりした。 pic.twitter.com/tspVL1Nugk— you (@youtoy) September 11, 2021 元になるプログラムと書きかえ後の内容 元のプログラム 冒頭に掲載した 1つ目の記事の中で、最終的に動かしたプログラムの JavaScript の部分は以下のとおりです。 規模感としては90行ほどです。 sketch.js let Engine = Matter.Engine, World = Matter.World, Bodies = Matter.Bodies; let engine; let world; let circles = []; let boundaries = []; let ground; function setup() { createCanvas(400, 400); engine = Engine.create(); world = engine.world; boundaries.push(new Boundary(150, 100, width * 0.6, 20, 0.3)); boundaries.push(new Boundary(250, 300, width * 0.6, 20, -0.3)); } function mouseDragged() { circles.push(new Circle(mouseX, mouseY, random(5, 10))); } function draw() { background(180); Engine.update(engine); for (let i = 0; i < circles.length; i++) { circles[i].show(); } for (let i = 0; i < boundaries.length; i++) { boundaries[i].show(); } } function Boundary(x, y, w, h, a) { let options = { friction: 0, restitution: 0.95, angle: a, isStatic: true, }; this.body = Bodies.rectangle(x, y, w, h, options); this.w = w; this.h = h; World.add(world, this.body); console.log(this.body); this.show = function () { let pos = this.body.position; let angle = this.body.angle; push(); translate(pos.x, pos.y); rotate(angle); rectMode(CENTER); strokeWeight(1); noStroke(); fill(0, 100, 200); rect(0, 0, this.w, this.h); pop(); }; } function Circle(x, y, r) { let options = { friction: 0, restitution: 0.95, }; this.body = Bodies.circle(x, y, r, options); this.r = r; World.add(world, this.body); this.show = function () { let pos = this.body.position; let angle = this.body.angle; push(); translate(pos.x, pos.y); rotate(angle); rectMode(CENTER); strokeWeight(1); stroke(255); fill(0, 0, 80); ellipse(0, 0, this.r * 2); pop(); }; } 書きかえ後のプログラム 今回、書きかえを行った後のプログラムは以下のとおりです。 規模感的にはおおよそ 60行ほどで、先ほどのものが三分の一くらいになりました。 sketch.js let boundaries = []; let circles = []; function setup() { createCanvas(400, 400); matter.init(); boundaries.push( matter.makeBarrier(150, 100, width * 0.6, 20, { angle: radians(20), friction: 0.02, restitution: 0.95, }) ); boundaries.push( matter.makeBarrier(250, 300, width * 0.6, 20, { angle: radians(-20), friction: 0.8, restitution: 0.95, }) ); } function draw() { background(180); push(); strokeWeight(1); noStroke(); fill(0, 100, 200); for (let i = 0; i < boundaries.length; i++) { boundaries[i].show(); } pop(); push(); strokeWeight(1); stroke(255); fill(0, 0, 80); for (let i = circles.length - 1; i >= 0; i--) { let b = circles[i]; b.show(); if (b.isOffCanvas()) { matter.forget(b); circles.splice(i, 1); } } pop(); } function mouseDragged() { circles.push( matter.makeBall(mouseX, mouseY, random(10, 20), { friction: 0.01, restitution: 0.95, }) ); } boundaries.push の部分をもう少しすっきりさせれば、もう少し短くなるかも。 書きかえの際に参考にしたプログラム 今回の書きかえをする際に参考にしたのは、以下の公式ページのサンプル「Tilted Platform」です。 Tilted Platform - Roll balls down a series of tilted platforms with realistic collisions by clicking your mouse. See it live! — View source code. この後、プログラムの書きかえ前後についての補足などを書いていきます。 プログラムの書きかえに関する補足 書きかえ前のおおまかな仕組み 書きかえ前のプログラムでは、Matter.js用の以下の変数を用意したり、 engine = Engine.create(); といった処理を書く必要がありました。 let Engine = Matter.Engine, World = Matter.World, Bodies = Matter.Bodies; また、以下のように円や傾いた床のような部分は、それを生成するための関数を別に用意したほうが良さそうなくらいの記述量があるようでした。 function Boundary(x, y, w, h, a) { let options = { friction: 0, restitution: 0.95, angle: a, isStatic: true, }; this.body = Bodies.rectangle(x, y, w, h, options); ... function Circle(x, y, r) { let options = { friction: 0, restitution: 0.95, }; this.body = Bodies.circle(x, y, r, options); ... それを使う部分は、以下が該当します。 function setup() { ... boundaries.push(new Boundary(150, 100, width * 0.6, 20, 0.3)); boundaries.push(new Boundary(250, 300, width * 0.6, 20, -0.3)); } function mouseDragged() { circles.push(new Circle(mouseX, mouseY, random(5, 10))); } そして、 draw() の中は、書きかえ前後で処理の流れ的には大幅な変更はない感じになりました。 (書きかえ後で、描画する際の色や枠の線の指定がこの中に入ってきたのはありますが) 上記の仕組みの中の各処理を変更する 初期化処理的な部分 書きかえ後のほうでは、初期化処理等は基本的に以下のみです。 これは参考にした公式サンプルに書いてあったので追加してみました。 matter.init(); こちらは、念のためドキュメントの以下の説明も見てみたのですが「書くのを忘れても大きな問題にならないもの」らしいです。 どうやら、説明を読むと「呼び出すのをオススメするけど、忘れてしまっていた場合は、何かのメソッドが呼ばれた際に呼び出しがされる」という感じのようです。 ●p5-matter 1.0.0 | Documentation  http://palmerpaul.com/p5-matter/docs/#matterinit 円や傾いた床の呼び出し 書きかえ前の円や傾いた床の呼び出しは上で書いていたとおりでした。 これが書きかえ後では、以下の matter.makeBall や matter.makeBarrier を呼び出すだけです。 書きかえ前の function Boundary(x, y, w, h, a) ・ function Circle(x, y, r) で書いていた処理は、ライブラリ内でうまく処理をしてくれているようです。 circles.push( matter.makeBall(mouseX, mouseY, random(10, 20), { friction: 0.01, restitution: 0.95, }) ); boundaries.push( matter.makeBarrier(250, 300, width * 0.6, 20, { angle: radians(-20), friction: 0.8, restitution: 0.95, }) ); この時、各パラメータは 4番目の引数として連想配列で渡してやる必要があるようです(Matter.js と同じ形です)。 それと、パラメータとして指定できるものは以下の Matter.js の Body のプロパティに関する APIドキュメントを見れば良さそうでした。  Body - Matter.js Physics Engine API Docs - matter-js 0.17.0 それと、これらを描画される際の処理で show() を使っています。 書きかえ前はこの部分の中身を実装していた形でしたが、書きかえ後はライブラリ内で中身をうまく隠蔽してくれている形になるようです。 円の削除 上で draw() の中の処理の流れは大幅な変更はない、と書いていたものの、書きかえ前になかった処理が加わっています。 それは、以下の forget() と splice() の部分です。 for (let i = circles.length - 1; i >= 0; i--) { let b = circles[i]; b.show(); if (b.isOffCanvas()) { matter.forget(b); circles.splice(i, 1); } } これを入れておかないと、画面外に出て行った円が配列に残ったままになっていました。 これを書いていて思ったのですが、「実は書きかえ前のものもこれに該当する処理が必要では?」という感じがします。 ここで出てくる p5-matter絡みの処理について、公式リファレンスの記載を掲載しておきます。 まとめ 今回、 p5.js と Matter.js を直接組み合わせる形のプログラムを、p5-matter を使って書きかえてみました。 その結果、描画まわりの処理など、ある程度は自前で実装する必要があった部分が、ライブラリ内での処理になるのでソースコードの内容がすっきりしました。 この後も、引き続き以下の公式ドキュメントなどを見つつ、活用法を探っていければと思います。 ●p5-matter 1.0.0 | Documentation  http://palmerpaul.com/p5-matter/docs/ 【追記】 MQTT との組み合わせ 以前、p5.js とリアルタイム通信の MQTT を組み合わせて、異なる 2つの異なるデバイスの画面が仮想的につながる、というものを試作してみていました。 その試作に、今回の p5-matter と Matter.js を使った物理演算を組み合わせてみました。 以前、 #p5js とリアルタイム通信(MQTT)を使って、異なる 2つのデバイスの画面を仮想的につなげてた件の続き、最近手を出し始めた物理演算エンジン(p5-matter + Matter.js)を組み込んでみました!2台の iPad の画面間でボールが行き来するのですが、2つの画面で物理演算エンジンが働いてます。 pic.twitter.com/vmdvhIBzwM— you (@youtoy) September 11, 2021
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

p5-matter を使って p5.js での物理演算エンジン(Matter.js)の利用を簡単化する【概要編】

今回の記事の内容は、以下の記事の最後に書いた「p5-matter」の話です。 ●p5.js と物理演算エンジン「Matter.js」の組み合わせをお試し - Qiita  https://qiita.com/youtoy/items/0b16f6fb3a1c76d64b68 前回の記事では、p5.js での描画処理に「物理演算エンジン Matter.js」を取り入れるという内容を、CodingTrain のリポジトリの中で公開されていたサンプルをベースにして進めました。 その際に、Matter.js の処理をいろいろ書く必要があったのですが、その処理を簡単化できる p5-matterというライブラリがあるらしいです。 (物理演算エンジンのお試しをあれこれやっている中、Twitter で「@takawo さんが OpenProcessing で過去に公開された作品」をツイートされているの見かけ、そこでロードされているライブラリの一覧を見ていた時に知りました) そこで、今回は p5-matter の公式の情報を見ていきつつ、簡単なお試しをしてみようと思います。 p5-matter の公式ページ関連 公式ページの情報をいろいろ見ていきたいところですが、まずは何か動くものを触れると楽しそうです。 スターターテンプレートを試す 中身を見てみる 公式ページを見ていくと、以下のように「Starter Template」という記載と、「Download the Starter Template! というリンク」がありました。 これをダウンロードして、中身を見てみます。 ファイルn中身の構成は、「index.html」・「sketch.js」・「3つのライブラリ」となっているようです。 その中の「index.html」と「sketch.js」の中身を見てみます。 index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Starter Template | p5-matter</title> </head> <body> <script src="lib/p5.js/0.5.11/p5.min.js"></script> <script src="lib/matter.js/0.12.0/matter.min.js"></script> <script src="lib/p5-matter/1.1.0/p5-matter.min.js"></script> <script src="sketch.js"></script> </body> </html> /* This is the start of a simple p5.js sketch using p5-matter. Use this as a template for creating your own sketches! */ var ball; var floor; function setup() { // put setup code here. createCanvas(600, 600); ball = matter.makeBall(width / 2, 40, 80); floor = matter.makeBarrier(width / 2, height, width, 50); } function draw() { // put the drawing code here background(0); fill(127); floor.show(); fill(255); ball.show(); } 前回の記事で登場したプログラムは実行内容が上記とは違いますが、対応関係がとれそうな部分を見て比べると、今回の JavaScript のプログラムが圧倒的に短そうです。 オンライン環境(p5.js Web Editor)で動かしてみる 前回の記事と同様に、オンラインの開発・実行環境である p5.js Web Editor を使って、こちらを動かしてみます。 p5.js Web Editor のデフォルトの index.html をもとに、p5.sound.min.js を削除し、matter.min.js(現時点の最新版を CDN から読み込み)と p5-matter.min.js を追加しました。 p5-matter.min.js は CDN からは読み込めなそうだったので、ファイルをアップロードして、それを読むようにしています(※ バージョンは 1.1.0)。 index.html <!DOCTYPE html> <html lang="en"> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.17.1/matter.min.js"></script><!-- 追加 --> <script src="./p5-matter.min.js"></script><!-- 1.1.0 追加 --> <link rel="stylesheet" type="text/css" href="style.css"> <meta charset="utf-8" /> </head> <body> <script src="sketch.js"></script> </body> </html> デフォルトの style.css は特に変更は加えていません。 そして、sketch.js の処理内容は以下としました。 少し makeBarrier の縦方向の位置をずらしたくらいで、全体的には元の内容ほぼそのままという感じです。 sketch.js let ball; let floor; function setup() { // put setup code here. createCanvas(640, 480); ball = matter.makeBall(width / 2, 40, 80); floor = matter.makeBarrier(width / 2, height-30, width, 50); } function draw() { // put the drawing code here background(0); fill(127); floor.show(); fill(255); ball.show(); } 上記の内容で処理を実行すると、以下のような結果が得られます。 公式サンプルを見てみる 公式のページを見ていくと、公式サンプルが見られるリンクがいろいろ掲載されてました。 どれも興味深かったですが、個人的には特に以下が面白かったです。 Lattice p5-matter( https://t.co/lnBkXp5oN2 )の公式サンプルの中の「Lattice」、面白い!⇒ マウス操作で動かせたりもする物理演算エンジンを使うと、 #p5js の描画でこんなこともできてしまうのか! pic.twitter.com/sOyX6CXpDA— you (@youtoy) September 10, 2021 公式ページに、Matter.js のカスタマイズ性をある程度制限して処理を簡単化、ということを書いていたように思いますが、p5-matter のサンプルを見る限りはいろいろと面白いことができそうです。 まとめ 今回、物理演算エンジンの Matter.js を p5.js と組み合わせて利用する際に、その処理を簡単化できる p5-matter を軽く試してみました。 また、公式で公開されているサンプルを見てみたり、その中の 1つを紹介してみたりしました。 公式サンプルを見て、何が実現できるかという情報はある程度見たものの、細かな部分は確認できてないので以下の公式ドキュメントを読んでみたりしつつ、また何か作品に取り入れられればと思います。 ●p5-matter 1.0.0 | Documentation  http://palmerpaul.com/p5-matter/docs/ 以下の仕組みとかに組み込むと、面白いことができたりしそうかな。 この前試した、 #p5js とリアルタイム通信(MQTT)の組み合わせで MacBook と iPad のそれぞれの画面間をつないだような処理をやった話の発展形!今度は、スマホ 2台の画面をつないだのですが、その2つの画面のつながる方向が、動的に縦方向だったり横方向だったり、変化するような実装にしてみた! pic.twitter.com/WmCev0oBfx— you (@youtoy) August 26, 2021 【追記】 活用編1 の記事を書きました ●p5-matter を使って p5.js での物理演算エンジン(Matter.js)の利用を簡単化する【活用編1】 - Qiita  https://qiita.com/youtoy/items/7fa6f6e6df2cf60133e6
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Markdownのリンクを「別タブで開く」に変換するスクリプト

Markdown記法では、リンクは次のように書きます。 [リンク先の説明](URL) これを、別タブで開くようにするには(Markdown記法ではHTMLを使えることを利用して)次のHTMLに変換すればOKです。 <a href="URL" target="_blank" rel="noopener noreferrer">リンク先の説明</a> Webブラウザーのエディター上(例えば、はてなブログ)でMarkdownを書くときに、この変換を一発でやってくれるスクリプトを作りました。ブックマークレットにして使っています。 javascript:(function(){ const t = document.querySelector('#body'); t.value = t.value.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'); })();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

社内の複数あるオンライン事務とバッチ事務を可視化してみた

はじめに  弊社の汎用機及びそれに群がるシステムでは、オンライン処理、バッチ処理が毎日動作しており、大、中、小合わせるとおよそ100種類以上の規模になります。その個々の事務はファイルの作成、更新、参照を繰り返して事務を進めるのですが、個々の事務は必ずしも独立していません。更に、全体を俯瞰する資料が存在しないため、システムの全体見直しを行う際に非常に困ります。 全体システムの見える化  今回、保守・業務担当にお願いし、各業務ごとに参照しているファイルのうち自業務のファイルではないものについてまとめてもらい、その情報の分析を networkx を用いて有効グラフ化した。なお,見直しにあたり,集約結果から各事務どうしの結合度を求めた。 作業の流れ 全事務のオンライン、バッチシステムの参照関係 方法:人力(機械化も可能だが,今回は業務担当にお願いした) 入力:業務知見 出力:参照関係ファイル オンライン、バッチ業務の参照関係を記した各々1ファイル(参照関係ファイル)にまとめた。 方法:プログラム 入力:参照関係ファイル 出力:参照関係(集約前)結果リスト 今回の業務見直しで 2 が集約されるため、その見直し結果として集約関係ファイルを作成した 方法:人力 入力:業務見直し方針 出力:集約関係ファイル 3の集約関係ファイルを参照し、1で作成した参照関係(集約前)結果リストを再度まとめなおした 方法:プログラム 入力:参照関係ファイル 出力:参照関係(集約後)結果リスト 4 で作成したファイルをもとにnetworkxの有効グラフで結んだ 方法:プログラム 入力:参照関係結果ファイル 出力:参照結果グラフ 材料 コーディングルール等は一切無視しているので、決して参考にしないでください。笑 参照関係ファイル(業務ごとに複数あり:一部抜粋) 参照関係作成プログラム(使い捨てプログラム作成(再帰テンプレあり):5分) sansho.js excel1 = new ActiveXObject("Excel.Application"); excelFile3 = "C:\\work\\analyzeInternal\\all.csv";//オンライン参照関係結果リスト excel3 = new ActiveXObject("Excel.Application"); excel3.Workbooks.Open(excelFile3, false, false); dstBook3 = excel3.Workbooks(excel3.Workbooks.Count); excelFile4 = "C:\\work\\analyzeInternal\\allb.csv";//バッチ参照関係結果リスト excel4 = new ActiveXObject("Excel.Application"); excel4.Workbooks.Open(excelFile4, false, false); dstBook4 = excel4.Workbooks(excel4.Workbooks.Count); excel1 = new ActiveXObject("Excel.Application"); Path = "C:\\work\\analyzeInternal"; createReferenceList(Path); function createReferenceList(target) { var fso = new ActiveXObject("Scripting.FileSystemObject"); var folders = fso.GetFolder(target); var e = new Enumerator(folders.files); var g = new Enumerator(folders.SubFolders); if((e.item() == null) && (g.item() == null)) return []; if(e.item() != null) { for(; !e.atEnd(); e.moveNext()) { if(e.item().name.match(/[^(all)].xlsx$/i)) { WScript.Echo(e.item().name); excel1.Workbooks.Open(e.item(), false, false); srcBook1 = excel1.Workbooks(excel1.Workbooks.Count); var gyoumu = srcBook1.WorkSheets(1).Cells(3,4).Value; WScript.Echo("オンライン:" + gyoumu); for(var i = 7; i < 100; i++) { var jimur = srcBook1.WorkSheets(1).Cells(i,3).Value; var jimuc = srcBook1.WorkSheets(1).Cells(i,4).Value; if((undefined != jimuc) && (undefined != jimur)) { WScript.Echo(jimur); WScript.Echo(jimuc); var row = dstBook3.WorkSheets(1).Range("A2:A104").find(jimur).Row; var column = dstBook3.WorkSheets(1).Range("B1:DP1").find(jimuc).Column; dstBook3.WorkSheets(1).Cells(row,column).Value = 1.0; } else { break; } } var gyoumu = srcBook1.WorkSheets(2).Cells(3,4).Value; WScript.Echo("バッチ:" + gyoumu); for(var i = 7; i < 100; i++) { var jimur = srcBook1.WorkSheets(2).Cells(i,3).Value; var jimuc = srcBook1.WorkSheets(2).Cells(i,4).Value; if((undefined != jimuc) && (undefined != jimur)) { WScript.Echo(jimur); WScript.Echo(jimuc); var row = dstBook4.WorkSheets(1).Range("A2:A104").find(jimur).Row; var column = dstBook4.WorkSheets(1).Range("B1:DP1").find(jimuc).Column; dstBook4.WorkSheets(1).Cells(row,column).Value = 1.0; } else { break; } } excel1.DisplayAlerts = false; excel1.Quit(); } } } if(g.item() == null) return []; for(; !g.atEnd(); g.moveNext()) { //解析不要なフォルダを確認しない。 if(!g.item().name.match(/old|test|bk|temp|log/i)) { var result = createReferenceList(g.item()); } } } excel3.DisplayAlerts = false; dstBook3.SaveAs(excelFile3); excel3.Quit(); excel4.DisplayAlerts = false; dstBook4.SaveAs(excelFile4); excel4.Quit(); 参照関係(集約前)結果リスト(一部抜粋) 集約関係ファイル(一部抜粋) 参照関係集約プログラム(使い捨てプログラム作成(流用):5分) shuyaku.js WScript.StdErr.WriteLine("start"); excel1 = new ActiveXObject("Excel.Application"); excelFile2 = "集約関係ファイル.csv"; excel2 = new ActiveXObject("Excel.Application"); excel2.Workbooks.Open(excelFile2, false, false); srcBook2 = excel2.Workbooks(excel2.Workbooks.Count); excelFile3 = "C:\\work\\analyzeInternal\\gyo.csv"; excel3 = new ActiveXObject("Excel.Application"); excel3.Workbooks.Open(excelFile3, false, false); dstBook3 = excel3.Workbooks(excel3.Workbooks.Count); excelFile4 = "C:\\work\\analyzeInternal\\gyob.csv"; excel4 = new ActiveXObject("Excel.Application"); excel4.Workbooks.Open(excelFile4, false, false); dstBook4 = excel4.Workbooks(excel4.Workbooks.Count); excel1 = new ActiveXObject("Excel.Application"); Path = "C:\\work\\analyzeInternal"; createReferenceList(Path); function createReferenceList(target) { var fso = new ActiveXObject("Scripting.FileSystemObject"); var folders = fso.GetFolder(target); var e = new Enumerator(folders.files); var g = new Enumerator(folders.SubFolders); if((e.item() == null) && (g.item() == null)) return []; if(e.item() != null) { for(; !e.atEnd(); e.moveNext()) { if(e.item().name.match(/[^(all)].xlsx$/i)) { WScript.Echo(e.item().name); excel1.Workbooks.Open(e.item(), false, false); srcBook1 = excel1.Workbooks(excel1.Workbooks.Count); var gyoumu = srcBook1.WorkSheets(1).Cells(3,4).Value; WScript.Echo("オンライン:" + gyoumu); for(var i = 7; i < 100; i++) { var frow = 0; var f2ro = 0; if(undefined == srcBook2.WorkSheets(1).Range("A2:A104").find(srcBook1.WorkSheets(1).Cells(i,3).Value)) { continue; } else { frow = srcBook2.WorkSheets(1).Range("A2:A104").find(srcBook1.WorkSheets(1).Cells(i,3).Value).Row; } if(undefined == srcBook2.WorkSheets(1).Range("A2:A104").find(srcBook1.WorkSheets(1).Cells(i,4).Value)) { continue; } else { f2row = srcBook2.WorkSheets(1).Range("A2:A104").find(srcBook1.WorkSheets(1).Cells(i,4).Value).Row; } var jimur = srcBook2.WorkSheets(1).Cells(frow,2).Value; var jimuc = srcBook2.WorkSheets(1).Cells(f2row,2).Value; if((undefined != jimuc) && (undefined != jimur)) { WScript.Echo(jimur); WScript.Echo(jimuc); var row = dstBook3.WorkSheets(1).Range("A2:A104").find(jimur).Row; var column = dstBook3.WorkSheets(1).Range("B1:DP1").find(jimuc).Column; if(row == column) { continue; } if(dstBook3.WorkSheets(1).Cells(row,column).Value == null) { dstBook3.WorkSheets(1).Cells(row,column).Value = 1; } else { dstBook3.WorkSheets(1).Cells(row,column).Value++; } } else { break; } } var gyoumu = srcBook1.WorkSheets(2).Cells(3,4).Value; WScript.Echo("バッチ:" + gyoumu); for(var i = 7; i < 100; i++) { var frow = 0; var f2ro = 0; if(undefined == srcBook2.WorkSheets(1).Range("A2:A104").find(srcBook1.WorkSheets(2).Cells(i,3).Value)) { continue; } else { frow = srcBook2.WorkSheets(1).Range("A2:A104").find(srcBook1.WorkSheets(2).Cells(i,3).Value).Row; } if(undefined == srcBook2.WorkSheets(1).Range("A2:A104").find(srcBook1.WorkSheets(2).Cells(i,4).Value)) { continue; } else { f2row = srcBook2.WorkSheets(1).Range("A2:A104").find(srcBook1.WorkSheets(2).Cells(i,4).Value).Row; } var jimur = srcBook2.WorkSheets(1).Cells(frow,2).Value; var jimuc = srcBook2.WorkSheets(1).Cells(f2row,2).Value; if((undefined != jimuc) && (undefined != jimur)) { WScript.Echo(jimur); WScript.Echo(jimuc); var row = dstBook4.WorkSheets(1).Range("A2:A104").find(jimur).Row; var column = dstBook4.WorkSheets(1).Range("B1:DP1").find(jimuc).Column; if(row == column) { continue; } if(dstBook4.WorkSheets(1).Cells(row,column).Value == null) { dstBook4.WorkSheets(1).Cells(row,column).Value = 1; } else { dstBook4.WorkSheets(1).Cells(row,column).Value++; } } else { break; } } excel1.DisplayAlerts = false; excel1.Quit(); } } } if(g.item() == null) return []; for(; !g.atEnd(); g.moveNext()) { //解析不要なフォルダを確認しない。 if(!g.item().name.match(/old|test|bk|temp|log/i)) { var result = createBatchFiles(g.item()); } } } excel2.DisplayAlerts = false; srcBook2.SaveAs(excelFile2); excel2.Quit(); excel3.DisplayAlerts = false; dstBook3.SaveAs(excelFile3); excel3.Quit(); excel4.DisplayAlerts = false; dstBook4.SaveAs(excelFile4); excel4.Quit(); 参照関係(集約後)結果リスト(一部抜粋) 各セルに入った数値は,集約後の各事務どうしの結合度となる グラフ化処理プログラム(2つのページ参考からコピペ:(ほぼ捜索時間)15分) networkflow.py import pandas as pd #バッチの場合 df_links = pd.read_csv('./gyob.csv') import networkx as nx import matplotlib.pyplot as plt import re import numpy as np G = nx.DiGraph() NUM = len(df_links.index) print(NUM) node_labels = ["TA","TE","TD","AA","CA","CC","GG","SS","BH","BN","IH","LA","WA","IU","MF","IT","IQ", "KK","TC","KS","IF","JG","ZK","SJ","JJ","BK","HG","KT","KM"] for i in range(0,NUM): for j in range(1,NUM): if df_links.iloc[i,j] >= 1.0: print("---") print("i:",i) print(node_labels[i]) print("j:",j) print(node_labels[j-1]) print(df_links.iloc[i,j]) print("---") G.add_edge(node_labels[i],node_labels[j-1]) pos = { #ノード数で分母を調整 n: (np.cos(2*i*np.pi/21), np.sin(2*i*np.pi/21)) for i, n in enumerate(G.nodes) } nx.draw_networkx(G,pos=pos,node_color="g",edge_color="g",font_color="w") plt.show() 結果 オンライン事務の関係グラフ バッチ事務の関係グラフ 参考 この2つのページがとても参考になりました。ありがとうございます。 分析ノート:NetworkXのグラフを可視化するときに頂点の座標を指定する コピペで試せる!Pythonのnetworkxでかっこいいネットワーク図を描いてみよう
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む