2015年10月20日火曜日

EWS(Exchange Web Service)接続仕様解説

はじめに

本ドキュメントはEWS(Exchange Web Service)を使用して、
スケジュールの取得、更新、削除の方法を解説する。
また、スケジュールのカスタムフィールドを埋め込んで予定を作成する方法も解説する。
EWSではスケジュールの他に連絡先、メールなどの取得機能がありますが、スケジュールに関するに機能にのみに絞って説明する。
Exchange Server 2010以降を対象とする。

基本的な方針

EWSはSOAP通信によってリクエストをPOSTで送ることで、Exchangeに持つ情報を操作可能。
また、httpsを使用するためSSL証明書をインストールする必要がある。
今回は通信時にBasic認証を行って認証するものとする。
EWSで使用できる命令一覧は以下を参照

スケジュール取得(FindItem, GetItem)

入力例1(時間を指定してスケジュールを取得)

FindItemによるスケジュール取得方法を示す。



  
    
      
        Default
      
      
      
        
      
    
  


BaseShape, CalendarView の値を変えることで、以下のように結果を指定できる。
BaseShape 意味
IdOnly IDのみを返す
Default デフォルトで規定されているプロパティを返す
AllProperties Exchange Business Logic レイヤで規定されているすべてのプロパティを返す
CalendarView 意味
MaxEntriesReturned 取得するスケジュールの最大件数を指定する。
StartDate 開始日時を指定する。表記はタイムゾーン(YYYY-MM-DDThh:mm:ss+09:00)
EndDate 終了日時を指定する。表記はタイムゾーン(YYYY-MM-DDThh:mm:ss+09:00)

応答例1

"--------------HEADER SECTION-------------------"
cache-control : private
transfer-encoding : chunked
content-type : text/xml; charset=utf-8
vary : Accept-Encoding
server : Microsoft-IIS/8.5
request-id : 2414b9a2-3098-4a78-afea-13ca70b8d541
x-targetbeserver : win-t79tuft08l9.hogehogenet.com
x-diaginfo : WIN-T79TUFT08L9
x-aspnet-version : 4.0.30319
set-cookie : exchangecookie=e1c788bc304a4876a75a8ceb9a15818e; expires=Mon, 07-Mar-2016 19:27:29 GMT; path=/; HttpOnly, X-BackEndCookie=S-1-5-21-2274913595-3117749821-1060021795-1135=u56Lnp2ejJqBx82bysedms7SxsfJmtLLms7I0sfLmsnSy87Im5rJms6dxs7OgYHNz87K0M/M0M/I387GxczIxc3G; expires=Sat, 07-Mar-2015 19:37:29 GMT; path=/EWS; secure; HttpOnly
x-powered-by : ASP.NET
x-feserver : WIN-T79TUFT08L9
date : Sat, 07 Mar 2015 19:27:28 GMT
connection : close
"--------------BODY SECTION-------------------"


  
    
  
  
    
      
        
          NoError
          
            
              
                
              
            
          
        
      
    
  

入力例2(GetItemによる取得)

スケジュールの固有ID, ChangeKeyが分かっていれば、GetItemでIDを指定して取得することが可能。
IDはオブジェクトに一意に付与される。ChangeKeyはオブジェクトのある特定の状態を表すもので、オブジェクトが更新されると、ChangeKeyも変化する。



  
    
      
        IdOnly
        
          
        
      
      
        
      
    
  

応答例2

"--------------HEADER SECTION-------------------"
cache-control : private
transfer-encoding : chunked
content-type : text/xml; charset=utf-8
vary : Accept-Encoding
server : Microsoft-IIS/8.5
request-id : e04c54a3-d851-4a91-aa57-4ff707bf57cb
x-targetbeserver : win-t79tuft08l9.hogehogenet.com
x-diaginfo : WIN-T79TUFT08L9
x-aspnet-version : 4.0.30319
set-cookie : exchangecookie=ada4db7f7ca448458f78c9a5cefb8b0e; expires=Mon, 07-Mar-2016 22:42:49 GMT; path=/; HttpOnly, X-BackEndCookie=S-1-5-21-2274913595-3117749821-1060021795-1135=u56Lnp2ejJqBx82bysedms7SxsfJmtLLms7I0sfLmsnSy87Im5rJms6dxs7OgYHNz87K0M/M0M/I383NxcrNxcvG; expires=Sat, 07-Mar-2015 22:52:49 GMT; path=/EWS; secure; HttpOnly
x-powered-by : ASP.NET
x-feserver : WIN-T79TUFT08L9
date : Sat, 07 Mar 2015 22:42:49 GMT
connection : close
"--------------BODY SECTION-------------------"


  
    
  
  
    
      
        
          NoError
          
            
              
              4月の予定
            
          
        
      
    
  

スケジュール新規作成(CreateItem)

CreateItemを使用してスケジュールが新規作成できる。

入力例3




  
    
      
        
      
      
        
          テスト予定(Powered by EWS)
          This is test meeting
          2015-04-04T14:00:00
          2015-04-04T15:00:00
          false
          Busy
        
      
    
  


応答例3

"--------------HEADER SECTION-------------------"
cache-control : private
transfer-encoding : chunked
content-type : text/xml; charset=utf-8
vary : Accept-Encoding
server : Microsoft-IIS/8.5
request-id : 809f50a6-f346-4d80-9695-6af66379c8d1
x-targetbeserver : win-t79tuft08l9.hogehogenet.com
x-diaginfo : WIN-T79TUFT08L9
x-aspnet-version : 4.0.30319
set-cookie : exchangecookie=573595eeeb4d498bb0102f09e8ef26a1; expires=Mon, 07-Mar-2016 22:51:22 GMT; path=/; HttpOnly, X-BackEndCookie=S-1-5-21-2274913595-3117749821-1060021795-1135=u56Lnp2ejJqBx82bysedms7SxsfJmtLLms7I0sfLmsnSy87Im5rJms6dxs7OgYHNz87K0M/M0M/I383Mxc/Oxc3M; expires=Sat, 07-Mar-2015 23:01:23 GMT; path=/EWS; secure; HttpOnly
x-powered-by : ASP.NET
x-feserver : WIN-T79TUFT08L9
date : Sat, 07 Mar 2015 22:51:22 GMT
connection : close
"--------------BODY SECTION-------------------"


  
    
  
  
    
      
        
          NoError
          
            
              
            
          
        
      
    
  

DeleteItemでIDを指定することで、削除可能。

入力例4




  
    
      
        
      
    
  

DeleteTypeを指定することで以下のように削除方法を指定できる。
DeleteType 意味
MaxEntriesReturned 物理的に消去する。
SoftDelete 回復可能な削除領域に移動させる(削除済みアイテムの保持が有効な場合)
MoveToDeletedItems アイテムを削除済みフォルダに移動させる。

応答例4

--------------HEADER SECTION-------------------
cache-control : private
transfer-encoding : chunked
content-type : text/xml; charset=utf-8
vary : Accept-Encoding
server : Microsoft-IIS/8.5
request-id : e08d3acf-3ee2-4a9b-b39e-28ac0229527b
x-targetbeserver : win-t79tuft08l9.hogehogenet.com
x-diaginfo : WIN-T79TUFT08L9
x-aspnet-version : 4.0.30319
set-cookie : exchangecookie=7070dbf7a6ef43938b076150169c3d3b; expires=Mon, 07-Mar-2016 17:57:21 GMT; path=/; HttpOnly, X-BackEndCookie=S-1-5-21-2274913595-3117749821-1060021795-1135=u56Lnp2ejJqBx82bysedms7SxsfJmtLLms7I0sfLmsnSy87Im5rJms6dxs7OgYHNz87K0M/M0M/I387Hxc/Ixc3O; expires=Sat, 07-Mar-2015 18:07:21 GMT; path=/EWS; secure; HttpOnly
x-powered-by : ASP.NET
x-feserver : WIN-T79TUFT08L9
date : Sat, 07 Mar 2015 17:57:20 GMT
connection : close
--------------BODY SECTION-------------------


  
    
  
  
    
      
        
          NoError
        
      
    
  

スケジュール更新(UpdateItem)

UpdateItemを使用することで、既存のスケジュールを更新可能。
IDとChangeKeyの両方を指定する必要がある。

入力例5




  
    
      
        
          
          
            
              
              
                Modified
              
            
          
        
      
    
  

更新内容が競合した場合、ConflictResolutionの指定により以下のような挙動となる。
ConflictResolution 意味
NeverOverwrite 競合が発生したら上書きしない。
AutoResolve 競合が発生したら自動的に解決を試みる。
AlwaysOverwrite 競合が発生しても常に上書きする。

応答例5

--------------HEADER SECTION-------------------
cache-control : private
transfer-encoding : chunked
content-type : text/xml; charset=utf-8
vary : Accept-Encoding
server : Microsoft-IIS/8.5
request-id : 0b9bccc5-c440-468a-8b40-cd2b186a583a
x-targetbeserver : win-t79tuft08l9.hogehogenet.com
x-diaginfo : WIN-T79TUFT08L9
x-aspnet-version : 4.0.30319
set-cookie : exchangecookie=f5fd2b1f008d45d3a0668b5c5dffebc3; expires=Mon, 07-Mar-2016 20:04:12 GMT; path=/; HttpOnly, X-BackEndCookie=S-1-5-21-2274913595-3117749821-1060021795-1135=u56Lnp2ejJqBx82bysedms7SxsfJmtLLms7I0sfLmsnSy87Im5rJms6dxs7OgYHNz87K0M/M0M/I383Pxc7Lxc7N; expires=Sat, 07-Mar-2015 20:14:12 GMT; path=/EWS; secure; HttpOnly
x-powered-by : ASP.NET
x-feserver : WIN-T79TUFT08L9
date : Sat, 07 Mar 2015 20:04:12 GMT
connection : close
--------------BODY SECTION-------------------


  
    
  
  
    
      
        
          NoError
          
            
              
            
          
        
      
    
  


カスタム項目を追加する場合

以下のようにExtendedPropertyを指定することで、カスタム項目を追加して
予定を新規作成できる。
PropertyNameに任意の名前を記述でき、PropertyTypeはデータの型を表す。
(以下の例ではAccountNoというカスタム項目を追加している)

入力例6




  
    
      
        
      
      
        
          CUSTOMテスト予定2(Powered by EWS)
          This is test meeting
          
            
            320
          
          2015-04-06T14:00:00+09:00
          2015-04-06T15:00:00+09:00
          false
          Busy
        
      
    
  

応答例6

"--------------HEADER SECTION-------------------"
cache-control : private
transfer-encoding : chunked
content-type : text/xml; charset=utf-8
vary : Accept-Encoding
server : Microsoft-IIS/8.5
request-id : c40b2b35-4aa8-4940-8548-3ea19ccede2a
x-targetbeserver : win-t79tuft08l9.hogehogenet.com
x-diaginfo : WIN-T79TUFT08L9
x-aspnet-version : 4.0.30319
set-cookie : exchangecookie=141d265651ac42aeab68137b65067650; expires=Sat, 12-Mar-2016 23:14:20 GMT; path=/; HttpOnly, X-BackEndCookie=S-1-5-21-2274913595-3117749821-1060021795-1135=u56Lnp2ejJqBx82bysedms7SxsfJmtLLms7I0sfLmsnSy87Im5rJms6dxs7OgYHNz87K0M/M0M7N383Mxc3Lxc3I; expires=Thu, 12-Mar-2015 23:24:27 GMT; path=/EWS; secure; HttpOnly
x-powered-by : ASP.NET
x-feserver : WIN-T79TUFT08L9
date : Thu, 12 Mar 2015 23:14:26 GMT
connection : close
"--------------BODY SECTION-------------------"


  
    
  
  
    
      
        
          NoError
          
            
              
            
          
        
      
    
  


カスタム項目が付与されたレコードを取得するには、カスタム項目で指定したPropertyNameを以下のように指定する必要がある。

入力例7





  
    
      
        IdOnly
        

            

        
      
      
        
      
    
  

応答例7

"--------------HEADER SECTION-------------------"
cache-control : private
transfer-encoding : chunked
content-type : text/xml; charset=utf-8
vary : Accept-Encoding
server : Microsoft-IIS/8.5
request-id : c98ce9a5-70d6-42bc-acfb-28f809617b43
x-targetbeserver : win-t79tuft08l9.hogehogenet.com
x-diaginfo : WIN-T79TUFT08L9
x-aspnet-version : 4.0.30319
set-cookie : exchangecookie=c2e612b25a6a495491dbb61db49a7e25; expires=Sat, 12-Mar-2016 23:19:35 GMT; path=/; HttpOnly, X-BackEndCookie=S-1-5-21-2274913595-3117749821-1060021795-1135=u56Lnp2ejJqBx82bysedms7SxsfJmtLLms7I0sfLmsnSy87Im5rJms6dxs7OgYHNz87K0M/M0M7N383Mxc3GxczJ; expires=Thu, 12-Mar-2015 23:29:36 GMT; path=/EWS; secure; HttpOnly
x-powered-by : ASP.NET
x-feserver : WIN-T79TUFT08L9
date : Thu, 12 Mar 2015 23:19:36 GMT
connection : close
"--------------BODY SECTION-------------------"


  
    
  
  
    
      
        
          NoError
          
            
              
              
                
                320
              
            
          
        
      
    
  

"--------------HEADER SECTION-------------------"
cache-control : private
transfer-encoding : chunked
content-type : text/xml; charset=utf-8
vary : Accept-Encoding
server : Microsoft-IIS/8.5
request-id : c40b2b35-4aa8-4940-8548-3ea19ccede2a
x-targetbeserver : win-t79tuft08l9.hogehogenet.com
x-diaginfo : WIN-T79TUFT08L9
x-aspnet-version : 4.0.30319
set-cookie : exchangecookie=141d265651ac42aeab68137b65067650; expires=Sat, 12-Mar-2016 23:14:20 GMT; path=/; HttpOnly, X-BackEndCookie=S-1-5-21-2274913595-3117749821-1060021795-1135=u56Lnp2ejJqBx82bysedms7SxsfJmtLLms7I0sfLmsnSy87Im5rJms6dxs7OgYHNz87K0M/M0M7N383Mxc3Lxc3I; expires=Thu, 12-Mar-2015 23:24:27 GMT; path=/EWS; secure; HttpOnly
x-powered-by : ASP.NET
x-feserver : WIN-T79TUFT08L9
date : Thu, 12 Mar 2015 23:14:26 GMT
connection : close
"--------------BODY SECTION-------------------"


  
    
  
  
    
      
        
          NoError
          
            
              
            
          
        
      
    
  

参考資料

参考文献
ひと目でわかる Exchange Server 2013

変更履歴

  • 1.0 初版作成

間違いなどありましたら指摘いただけると幸いです。

Written with StackEdit.

2015年10月14日水曜日

ISUCON5のベンチを動かすメモ


ISUCON5の予選問題を動かしてみたいけどどうやったらいいのかよくわからん人用のメモ。

GCPアカウント作成~イメージ作成まではレギュレーションに書いてあるので省略。

$ sudo su - isucon
$ vi .bashrc

 //以下を追記
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
alias ll="ls -la"
alias vi="vim"
 //.bashrc反映
$ . .bashrc
 // リポジトリをクローン
$ git clone https://github.com/isucon/isucon5-qualify.git

$ cd isucon5-qualify/bench

 //ビルド
$ gradle compileJava

 //jq入れる
$ sudo apt-get install jq

 //ベンチ実行
$  ( curl localhost/initialize;cd ~/isucon5-qualify/bench; cat ~/isucon5-qualify/webapp/script/testsets/testsets.json  | jq .[0] | gradle run )

自分の環境ではなぜかベンチがJSTの時刻を取得できなくてうまく動いてなかったので、
以下の修正を入れた。

$vi ./bench/src/main/java/net/isucon/isucon5q/bench/scenario/BootstrapChecker.java
LocalDateTime beforeUpdates = LocalDateTime.now().withNano(0);
LocalDateTime afterUpdates = LocalDateTime.now().withNano(0);

 => 以下のようにZoneIdを指定
//ZoneIdをインポート
import java.time.ZoneId;

ZoneId zoneId = ZoneId.of("Asia/Tokyo");
LocalDateTime beforeUpdates = LocalDateTime.now(zoneId).withNano(0);
LocalDateTime afterUpdates = LocalDateTime.now(zoneId).withNano(0);


以上。

2015年9月27日日曜日

Excel系SEがISUCON5に参加してみた



ISUCONなる面白そうなコンテストがあったので、同僚を誘って計三人で参加してみた。
やったこと、感想などを残しておく。

1.ISUCONとは

お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトル。 詳しくはこちら。

2.準備編

今年6月ぐらいからISUCONが開催されることを知ったので、三週間に1回ぐらいみんなで過去門をやるぐらいのまったりペースで練習開始。普段こういうチューニングはやらいので、みんな手探りの状態からとりあえずスタート。最終的に5回ぐらい集まって練習会を実施し、行ったのは大体以下の内容。

  • 1回目・・・ とりあえずISUCON4予選過去問をやってみる。AWSの設定やら試行錯誤でみんな好き勝手にいじる。模範解答を参考にnginxやRedisを入れてスコアをあげてみる。
  • 2回目,3回目・・・ ISUCON4予選問題を各自でいじってみて、結果を持ち寄る。node + Redis構成にしてMySQLを完全に取っ払うことで、当時の予選通過スコアぐらいまで出せて満足。
  • 4回目・・・ ISUCON3予選問題をみんなでやる。そろそろ本番はどういう構成で望むのかを議論。みんなそれほど得意な言語はなかったので、最大公約数的にRubyを選択し、DBは部分的にRedisかもしくはSQLite(インメモリ)で行こうという方針で検討。
  • 5回目・・・ ISUCON4予選問題をRuby+Redis+nginx で構成するように模擬本番形式で実施。Google Cloud Platformで動くISUCON4予選イメージが配布されていたので、そちらを使用する。スコア送信用本番サーバ1台と、各自の開発用サーバ1台*3の構成で実施。ファイルの受け渡しはGithub上のリポジトリでやり取りするように取り決め。SSH鍵の作成やRuby, Redis, nginx の設定ファイルテンプレをリポジトリに登録。
# レギュレーションを見て、予選終了までリポジトリ公開は禁止だったので、慌ててgithubアカウントをプライベートに変更(合わせてGithubアカウントをアップグレード)

また、予選をチーム名"本業:Excel職人" で登録。

3.本番当日編

10開始予定だったので、9時に集合して買出しなど済ませ、準備に入る。
その後アナウンスがあり11時開始に変更となったので、それまで待機。ISUCON4のイメージを上げて復習。

11時からポータルサイトにアクセスが解禁され、VMイメージが公開されたので、早速展開して接続。その際に"uname -a" を打ったときに衝撃が走る。

Linux isucon5-production 3.19.0-21-generic #21-Ubuntu SMP
Ubuntuだと・・・(´゚д゚`)


ISUCON4予選も3予選もCentOSだったので、Ubuntuはノーマークだった。しかも普段CentOSしか触っていなく、Ubuntuをサーバ用途でいじったのは何年ぶりかわからない。
しかもサービスはsystemdで管理されているし。。これログどうやってみるんだっけ。。
※(追記) レギュレーションにUbuntuって追記されてましたね。。恥ずかしい。。

学生のころの期末テスト開始直後で問題用紙を裏返して見た直後に"ここ出すんかい"
と試験勉強範囲を外されたときと似た思いだった。

お題はSNS系のサイトだった。過去の予選に比べかなり複雑な印象を受けた。
DBのテーブルも今まで2~3個だったけど、今回は7つ。

とりあえず1時間半程度かけてみんなでコードリーディングしながら、改善点を洗い出していく。
最終的に変更できたのは以下の内容。

  • nginxでの静的ファイルのキャッシュ。nginxやunicornの稼動プロセス数の変更。通信のgzip化
  • tcp接続からunixドメインソケットを使用するよう変更
  • 足跡の処理をMySQL -> Redisに変更
  • 日記のentryテーブルのbodyカラムにタイトルと本文が入っているために、分離
  • usersとsaltsテーブルの統合
  • いけてないMySQLクエリを粛々と改善。インデックスの検討
  • 不要なサービスの停止
上記のことをやっていたらいつの間にか8時間たってしまった。
スコアは上位陣とは程遠いもので、初期スコアよりはまあ改善は見られるものの、もう少し出せればよかった。。上位陣の模範解答が気になる。

練習でやろうとしていたことは最低限できたような気がしたが、修正スピードが遅かった。
もう少し多くの部分をRedisに置き換えたかったが、ベンチに怒られて最終的にコミットできず。
また練習ではベンチマークがまわし放題だったが、本番ではベンチの実行リクエストがキューに
たまるので好き放題まわせなくて、修正に戸惑う場面もあった。

今回の練習でsinatoraとかnodeとかRedisとかnginxとか普段触らないものを勉強できたのでいい機会だった。
前回よりお題が複雑で、運営さんも大変そうだった。

似たようなイベントがあればまた参加したい。


※上記ロゴ画像は本家より拝借しております。問題ありましたら削除しますのでご連絡下さい



2014年6月9日月曜日

AndEngineの使い方(インストールメモ)

Andoridの2Dゲームエンジン"AndEngine"が面白そうなので、使い方をメモ




当方の実行環境は以下の通り。

  • IntelliJ IDEA Community Edition 13.1.2
    • Android開発用のプラグインを入れておくこと
  • 以下のバージョンのAndEngine
    • commit: 720897f99d2c56ba357e8fe361454bd8d88c37ed
  • AneEngineExample
    • commit: 75b657b2cd7e1153e005a917753b6d0390e034d5
  • その他GitHubに公開されているExtension全て

1.ソースコードの入手

Githubにソースコードが公開されているので、AndEngine本体に加えて、サンプルコードをExtensionをすべてcloneする。
同一のディレクトリにまとめてcloneしておくと便利。

2.プロジェクトの作成

IntelliJの"import project"でAndEngineExampleのプロジェクトをインポートする。
参照するライブラリはInnteliJが自動で解決してくれるみたい。便利。

3.サンプルプログラムの実行

ExampleLauncherを実行すればさまざまサンプルが実行できる。

そのままビルドすると、いくつかエラーが発生したので、以下のように修正





  • java: シンボルを見つけられません[シンボル:   クラス DrawMode




  • インポートするディレクトリを変更 

  • import org.andengine.entity.primitive.vbo.DrawMode; 
    → import org.andengine.entity.primitive.DrawMode;





  • TextBreakExampleのTextOptionの引数が逆になっているので修正



  • BoundCameraExampleのAnimatedSprite作成時に、animateの実行を別の文に分ける。
  • final AnimatedSprite face = new AnimatedSprite(pX, pY, this.mBoxFaceTextureRegion, this.getVertexBufferObjectManager()).animate(100);



    これでビルドできるようになり、
    これらのサンプルを参考にすればすぐにある程度ゲームが作れそう。


    2013年12月21日土曜日

    ブラックジャックのシミュレータをjavascriptで書いてみた

    ブラックジャックのシミュレータは以前も作ったことがあるけど、今回はブラウザでも実行できるようjavascriptで作ってみた。 途中で力尽きしまったので、ディーラーのシミュレーション部分のみ作成。 概要は以下のとおり

    • ディーラーのアップカードを2~10, Aceのいずれかを選択する
    • 17以上になるまでカードを引く
    • 最終的なディーラーハンドの出現回数と割合を計算する


    結果の見方

    たとえば、ディーラーのアップカードが10のとき、バーストする確率は約22~23%というが分かる。 ということは、プレイヤーカードが16であったとしてもスタンドすればほぼ8割方負けしまうので、引いた方がまだましという判断になる。

    シミュレータの場所

    シミュレータのページはこちら

    2013年8月31日土曜日

    BlackJackシミュレータ作ってみた

    ブラックジャックのそれぞれの手の場合にどれくらいの期待値で
    勝てるのかを調べるために、Scalaでモンテカルロシミュレーションプログラムを作ってみた。


    • プレイヤーの手札が8~20の時に対して、ディーラーのアップカードが2~Aのそれぞれに対して、HIT/STAND/DOUBLEDOWNを500000回繰り返したときの期待値を求める。
    • カードは無限にあるものとする(カウンティングの要素は含まない)
    • 乱数は擬似乱数を使用する
    • サレンダーは考慮しない(アーリーサレンダーとレイトサレンダーを分けるのがしんどいので)
    • ディーラーはソフト17スタンドとする

    結果



     こんな感じになった。表の各数値はHIT / STAND / DOUBLE DOWN をずっと繰り返したときの期待値です。各セル内の最大値となるものが、最適戦略となる。


    他のサイトとの比較

     このあたりに掲載されているベーシックストラテジーと比較すると、一部微妙に違いがあるが、このあたりは乱数の性能の限界なのかもしれない。




    作成したプログラム


    GitHubに置いています。