画像の解像度を変換する

【注意】画像エンコードのことは全然詳しくないので、本質的に正しいやり方なのか不明。

System.Drawing.Image で読み込んで .SetResolution してもいいのだが、

var image = Image.FromFile(path);
image.SetResolution(dpiX, dpiY);

System.Drawing は ASP.NET で非推奨らしい (メモリリークするとかいう噂) ので、 Windows Imaging Components を使う。

void Encode(Stream fromBitmapStream, Stream toBitmapStream, double dpiX, double dpiY)
{
  var decoder = BitmapDecoder.Create(fromBitmapStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
  var frame = decoder.Frames.First();

  var stride = frame.PixelWidth * 4; // 1 ピクセルあたり 4 バイト
  var pixelData = new byte[frame.PixelHeight * stride];
  frame.CopyPixels(pixelData, stride, 0);

  var source = BitmapSource.Create(frame.PixelWidth, frame.PixelHeight, dpiX, dpiY, frame.Format, frame.Palette, pixelData, stride);

  var encoder = new JpegBitmapEncoder() { Frames = { BitmapFrame.Create(source) } };
  encoder.Save(toBitmapStream);
}

PresentationCore と WindowsBase の参照が必要。で using System.Windows.Media.Imaging; する。 ちなみに BitmapDecoder とかいってるけど Jpeg でも可。

参考

2016年に読んだ本から再読したいものを5冊挙げる

去年に引き続き。


自分の中に毒を持て―あなたは“常識人間"を捨てられるか (青春文庫)

不連続的に人生を変えるレベルの一冊。迷ったら困難な方を選べ。困難は、経験して必ずしも成長するわけじゃない。困難を味わうことが人生の醍醐味だ。
……なんて書いていたかどうか覚えていないが、この本を読んで、自分でも思索に耽った結果、そう思っている。 

ファシリテーションの教科書: 組織を活性化させるコミュニケーションとリーダーシップ

 ファシリテーションを体系的に解説。迷ったときに読む。 

エンジニアのための図解思考 再入門講座

図解思考の技術というかそういう話ももちろんだけど、文章の質が異様に高いので、その点でも勉強になる。 

レガシーコード改善ガイド

これは俺たちの教科書だ。常に机に置いておきたい。

ひとり飲み飯 肴かな (NICHIBUN BUNKO)

既にめっちゃ再読してる。すいません。


去年よりもビジネス書をよく読んだ。小説も増やしたい。哲学書はあんまり読まなくなったなぁ。

定期的に GitHub API を叩いて Slack にポストしたい (3)

最終回

メッセージを Slack に投げる

function sendToSlack(message, attachments) {
  var url = "https://slack.com/api/chat.postMessage";
  var token = "token" // Slack のアクセストークン
  var channel = "#channel"; // 投稿する先のチャンネル
  var text = message; // 本文
  var username = "GitHub"; // Slack に投稿する BOT の名前
  var parse = "full"; // URL エスケープする
  var icon_emoji = ":emoji:"; // emoji 名
  
  var payload = {
    "token" : token,
    "channel" : channel,
    "text" : text,
    "username" : username,
    "parse" : parse,
    "icon_emoji" : icon_emoji,
    "attachments" : JSON.stringify(attachments)
  };
  
  var method = "post";
  var params = {
    "method" : method,
    "payload" : payload
  };
  
  var response = UrlFetchApp.fetch(url, params);
}

各 PR の情報を全部 message として組み立ててもいいのだけど attachment を使うとよりカッコよく一覧化できる。 ここ で試せる!

// 各 PR を attachment として配列で返す。
function createAttachments(pulls) {      
  var attachments = [];
  for (var i = 0; i < pulls.length; i++) {
    var pull = pulls[i];
    
    // GAS はいまのところ yyyy/mm/dd hh:mm:ss.mmm じゃないとパースできないのでミリ秒をくっつける。
    var createdAtDate = new Date(pull["createdAt"].replace("Z", ".000Z"));
    var createdAtString = 
      (createdAtDate.getMonth() + 1) + "/" + 
      createdAtDate.getDate() + " " +
      ("0" + createdAtDate.getHours()).slice(-2) + ":" +
      ("0" + createdAtDate.getMinutes()).slice(-2);
    
    var attachment = {
      "color": "#000000",
      "author_name": pull["user"],
      "author_icon": pull["avatar_url"],
      "title": pull["title"],
      "title_link": pull["url"],
      "footer": pull["commentCount"] + " comments ( +" + pull["newCommentCount"]  +" ), created at " + createdAtString,
    };
    
    attachments.push(attachment);
  }
  
  return attachments;
}

で、 PR 取得したらメッセージ付けて Slack に流す。

function notify() {
  var pulls = getPulls();
  if (pulls.length == 0) {
    return;
  }
  
  var message = pulls.length + " 個の PR が更新されました。"
  var attachments = createAttachments(pulls);
  
  sendToSlack(message, attachments);
}

cron 的な設定

解説や注意点など

  • チャンネルじゃなくて DM に送りたいときは # じゃなくて @ にするらしい。
  • Google Apps Script の Date は生 js と微妙に仕様が違うようで、コメントにも書いている以外にも色々ありそう。
  • attachment で日時を表示したいとき、自前で文字列にくっつけなくても ts に UNIX timestamp を指定すると出してくれるのだけど「Dec 29th at 7:44 PM」とかで表示されて我々にとって直感的じゃないのでやめた。
  • Slack の API はドキュメントが充実してるのでとっつきやすいですね。

定期的に GitHub API を叩いて Slack にポストしたい (2)

前回 では PR 一覧を取得したが、ラベルでの絞込やコメントが追加されたかどうかを判定できていなかった。

PR のラベル、コメント数を取得

  • PR 取得の API ではラベルとコメントを取得できないので別途 API をたたく。
/// ある PR に付与されたラベルを取得する
function getLabels(number, owner, repo, options) {
  var url = "https://api.github.com/repos/" + owner + "/" + repo + "/issues/" + number + "/labels?per_page=100";
  var response = UrlFetchApp.fetch(url, options);
  var json = JSON.parse(response.getContentText());
  
  var labels = [];
  for (var i = 0; i < json.length; i++) {
    var hash = json[i];
    labels.push(hash["name"]);
  }
  
  return labels;
}

// コメント数を取得
function countComments(number, owner, repo, options) {
  var url1 = "https://api.github.com/repos/" + owner + "/" + repo + "/issues/" + number + "/comments?per_page=200";
  var response1 = UrlFetchApp.fetch(url1, options);
  var comments1 = JSON.parse(response1.getContentText());
    
  var url2 = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls/" + number + "/comments?per_page=200";
  var response2 = UrlFetchApp.fetch(url2, options);
  var comments2 = JSON.parse(response2.getContentText());
  
  return comments1.length + comments2.length;
}

前回取得時のコメント数を保存

  • コメント件数を覚えておいて、前回の件数より増えていたら通知対象にする、などに使う。
// PR 単位にコメント件数を PropertiesService で覚えておき、前回実行時からの差分を取得する。
function getUpdatedInfo(number, commentCount) {

  var key = number;
  var oldPull = JSON.parse(PropertiesService.getScriptProperties().getProperty(key));
  
  // 新しく作成された、またはコメントが追加された PR 
  var isCreated = oldPull == null;
  if(isCreated || commentCount > oldPull.commentCount) {
    var value = JSON.stringify({ 
      "commentCount" : commentCount
    });
    PropertiesService.getScriptProperties().setProperty(key, value);
    
    // 新しく作成された PR
    if(isCreated) {
      return {
        "isCreated": true,
        "newCommentCount": commentCount
      };
    }
    
    // コメントが追加された PR
    return {
      "isCreated": false,
      "newCommentCount": commentCount - oldPull.commentCount
    };
  }
    
  // 既存でコメントが追加されていない PR
  return {
    "isCreated": false,
    "newCommentCount": 0
  };
}

解説や注意点など

  • ラベルの取得はそのまんま。 PR は issue の一種という扱いらしく、 issue の API は PR でも使える。らしい。
  • コメントは、本文へのコメント(issues)とレビューコメント(pulls)で別に計算されるらしいのでそれぞれで取得する。
  • PropertiesService を使うと key-value でデータを write/read できる。この値は、画面からも確認できて、 Google Apps Script の「ファイル - プロジェクトのプロパティ - スクリプトのプロパティ」で表示される。素敵!

定期的に GitHub API を叩いて Slack にポストしたい (1)

発端

  • GitHub でレビューコメントきたら Slack に通知ほしい
  • かといって全部のコメント通知されてもウザい
  • 定期的に GitHub をみにいってコメントが増えてたら Slack に投げる、みたいなの bot を作ろう
  • 全3回の予定。

調べた

  • GitHub Slack」で検索すると大体「Node.js + Hubot 安定」みたいな記事ばっかりヒットする。でもサーバー立てたりダルい
  • Google Apps Script って手があるらしい
  • Google Spreadsheet でマクロ書くときに使うアレ
  • ほぼ js
  • 単体でも組めて cron 的に使える
  • Google Drive -> 新規 -> その他 -> Google Apps Script (初回は「アプリを追加」で追加する必要あり)

GitHub API をたたく

  • 今回は GitHub から PR を取得するだけ
  • ラベルの絞り込み、コメントの取得、 Slack に投げるところは次回以降
  • ラベルは何に使ってるかっていうと、投げるチャンネルを決めるため。チーム内の PR には特定のラベルをつける、という運用をしていて、拾った PR はチームのチャンネルに投げる想定。
function getPulls() {
  var owner = "owner";
  var repo = "repo";
  var token = "accessToken" // GitHub で発行
  
  var options =
  {
    "method" : "get",
    "headers" : {
      "Authorization": "token " + token 
    }
  };
  
  // open な PR を全て取得して、そのあとラベルで絞り込む。
  // per_page を指定しないと 30 件しか取れない。
  var url = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls?per_page=100";
  var response = UrlFetchApp.fetch(url, options);
  var json = JSON.parse(response.getContentText());

  var results = [];
  for (var i = 0; i < json.length; i++) {
    var hash = json[i];
    
    // ラベルの絞込をここでやる
    var labels = getLabels(hash["number"], owner, repo, options);
      if (hash && labels.indexOf(targetLabel) >= 0) {
      var commentCount = countComments(hash["number"], owner, repo, options);
      
      // 新しく追加された、またはコメントが追加されたもののみ対象とする。
      var updatedInfo = getUpdatedInfo(hash["number"], commentCount);
      var isUpdated = updatedInfo.isCreated || updatedInfo.newCommentCount > 0
      if (!isUpdated) {
        continue;
      }
      
      // reviewer requests の API は preview なのでまだ使わない
      //var reviewers = getReviewers(hash["number"], owner, repo, options);
      
      results.push({
        "url": hash["html_url"],
        "title": hash["title"],
        "user": hash["user"]["login"],
        "createdAt": hash["created_at"],
        "avatar_url": hash["user"]["avatar_url"],
        "number": hash["number"],
        "commentCount": commentCount,
        "newCommentCount": updatedInfo.newCommentCount
      });
    }
  }
  
  return results;
}

解説とか注意点とか

TransactionScope のネスト

System.Transactions.TransactionScope は Complete を呼ばない限り Dispose されたときにロールバックしてくれる。 テストとか例外処理とかで便利だ。

using (var ts = new TransactionScope())
{
  // DB に書き込み

  ts.Complete();
}

ネストしてもつかえる。このときネスト内で new された TransactionScope すべてが Complete されてはじめてコミットされる。

void WriteParent(string connectionString) {
  using (var ts = new TransactionScope())
  {
    WriteChild1(); // この時点ではコミットされない

    WriteChild2(); // まだ

    ts.Complete(); // ここでようやくコミット
  }
}

void WriteChild1() {
  using (var ts = new TransactionScope())
  {
    // 書き込み

    ts.Complete();
  }
}

void WriteChild2() {
  using (var ts = new TransactionScope())
  {
    // 書き込み

    ts.Complete();
  }
}

ここで、子の TransactionScope が Complete されずに Dispose されるとその時点でロールバックされるので、親で Complete しようとすると TransactionAbortedException が発生してしまう。 例えば子で以下のように「書き込みが発生するのは特定の場合だけだから、そうじゃなかったら Complete 呼ばない」とかすると NG。

  using (var ts = new TransactionScope())
  {
    if(shouldUpdate) 
    {
      // 書き込み

      ts.Complete();
    }
  }

(TransactionScope はコンストラクターで新規に作るか既存を流用するを指定できる (TransactionScopeOption) 。このへんの指定で挙動が変わるかも?)

参考

トランザクション スコープを使用した暗黙的なトランザクションの実装

話しをきく

(この記事は Sansan Advent Calendar 2016 - Qiita 8日目の記事です。
技術系の記事にはさまれてますが全然違うことを書きます。)

 

みなさん、コミュニケーションは得意ですか?私は苦手です!

しかし、苦手意識というのは人を成長させるもので、私も昔よりはコミュニケーションが上手くなっている実感があります。エンジニアにとってコミュニケーション力は生命線。今回は対面でのコミュニケーションにおいて、特に「話をきく」側として、自分がどんなことを実践しているかを振り返ってみました。

「話をきく」ということ

まず、話をきく目的を整理しましょう。

場には話し手と聞き手がいるので、双方にメリットがあるはずです。まず聞き手にとっては「話し手から新しい情報を授かること」があるでしょう。一方、話し手にとっては「聞き手と会話することで新たな気付きを得ること」があると思います。ラバーダッキングという言葉もありますね。あるいは、単に「楽しむこと」もあるでしょう。

ということで、聞き手として上記の目的に寄与するためには、

  • いかに話し手から情報を得るか
  • いかに話し手に新たな気付きを与えるか
  • いかに会話を楽しむか

が大事だと言ってよいでしょう。ここからは、私が実践していることが、それぞれ上記をどう達成しているかをみていきます。

相づち

なるべくバリエーションが豊富になるようにしています。そのほうがより会話が盛り上がる気がします。定番は「はい」「ええ」「うんうん」「なるほど」あたり。これらは、話を順方向に進ませるものです。話のスピードを上げたいときは、この相づちのスピードもテンションも上がります。オウム返しも有効とよくいいますが、場合によっては「その表現であってる?」ってニュアンスになりかねないので、万能ではないと個人的には思います。

一方で話を別の方向にもっていきたい、あるいはその場に立ち止まりたい場合があります。そのときは「ん?」と疑問を表明したり、「えーと」と、「自分の解釈を確認しようとしている」意を表明したりします。または「んー……」のように、「理解しようとしているが、別の表現で説明してもらいたい」ことを匂わせることもあります。

これらは、このように文章で読むと「いや、そんな回りくどいことせずに言葉で伝えろよ!」という思いにもなるでしょうが、会話をしている間はそこに論理があるわけではなくて、そのときのモヤモヤした気持ちをいま言語かするとこのように表せる、ということです。また、「んー……」といって相手からリアクションがなければ「別の表現で説明するとどうなりますか」と聞きます。つまり、相づちで相手がこちらの (形になっていない) 意思を汲み取ってくれたら儲けものくらいの感覚です。

どうやら (私にとって) 相づちは「いかに話し手から情報を得るか」を重視しているようですね。 +α の要素として、会話が盛り上がることで「いかに会話を楽しむか」にも関わってきそうです。

うなづき

話し手が一人で聞き手が複数人の場合に多用します。一人が全体に向かって話すというのは不安なものです。そういうときに「聞いてます、理解しています」とサインを出す人がいたら話し手は安心しますよね。

ただし、あまり回数が多いのも話し手にプレッシャーを与えるのではないかと思っています。うなづきは、話が順方向に進むのを期待していることの表明です (だと僕は思っています) 。なので、例えば「現在こんなタスクをやっています」という話をしているときに「でも実は結構遅れてて……」という話を切出しづらくなるんじゃないかな、という仮説を最近たてています。

また、話し手が明確に同意を求めている場合、例えば「~と思います。よいでしょうか?」と聞き手に投げかけている場合は、うなづきよりも声に出したほうが良さそうです。

こうしてみると、うなづきは上述の目的にイマイチはまらないですね。一対多を想定しているので一方向のコミュニケーションが前提になっているからでしょうか?一対一におけるうなづきについても、考えてみる余地があります。

質問

質問はコミュニケーションにおいて大変重要だと思っています。先に目的の話になりますが、質問は聞き手の理解に繋がるのは当然のこと、話し手も質問されてみて初めて気づくことも沢山あるでしょう。

質問にもいろいろ観点があります。「話し手が伝えたつもりでいるかどうか」「いまの話に沿っているかどうか」「聞き手が実は答えを知っているかどうか」「オープンかクローズか」などなど。……ここまで書いてみましたが話が広がりすぎるのでやめときます。

どんな質問をするか、については、あまり深く考えていません。まだギリ若手の部類に入るので非常識な質問をしても良いと思っています。

一対一のときは、タイミングを重要視しています。あまり頻繁だとスムーズさが失われますし、最後にまとめて、だと途中の会話が無駄になったりします。話が行き詰まりそうなポイントに差し込むのが理想かなと思います。

質問については↓の本が面白かったので載せておきます。 

「良い質問」をする技術

「良い質問」をする技術

 

 

笑い

基本的に、笑えるタイミングがあったら笑うようにしています。周りの人からは「よく笑うよね」と言われます。やっぱり笑いは場を和ませる効果がある。エンジニア同士でも、「こんなバグがある!」ってときに「マジかよ……」となるか「マジかよwww」となるかで違いますよね。

プレゼンなんかの場合は、「ここで笑ってほしいんだろうけど、イマイチ笑いづらい」ポイントで積極的に笑うようにしています (スベったらそれはそれで面白いパターンもあるのでそういうときは避けますが) 。こういうケースって結構あって、面白さは聞き手に伝わってるんだけどオチが先にスライドに出てるとか、笑うタイミングがわかりづらいっていう状況ですね。一人が笑うと話し手も安心するし、他の聞き手も笑いやすくなる。

ただし、あえて笑わないようにすることもあります。最近たてている仮説ですが、笑っている瞬間、人は頭を使っていないのでは?という考えが背景にあります。振り返ってみると、とくに周りに人がみんな笑っているときは笑わないようにしている気がします (飲み会とかは別ですが) 。もしかすると、「みんなが笑っている間に頭を使うと、みんなが気づいていないことを指摘できる」ということを経験的に知っているのかもしれません (文章にするとめちゃめちゃ気持ち悪いですね) 。これについては今後むきあっていきます。

 

以上、きわめて内省的な記事でしたが、最後まで読んでくださってありがとうございました。あなたはコミュニケーションでどんなことを実践していますか?