xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

1 次元配列から 2 次元辞書を作ろう

以前 2 次元配列の要素をインデックス付きで 1 次元配列に落とす、というのを紹介しました。

となると、その逆もやってみたくなりませんか…?*1

拡張メソッドを作る

通常の ToDictionary メソッドはひとつのプロパティをキーにして辞書を作りますが、このキーをふたつにすればよいでしょう。以下のような感じになります。

//--- キーだけ選択してそのまま要素を格納するバージョン
public static Dictionary<TKeyX, Dictionary<TKeyY, TSource>> ToDictionary2<TSource, TKeyX, TKeyY>
    (
        this IEnumerable<TSource> self,
        Func<TSource, TKeyX> xSelector,
        Func<TSource, TKeyY> ySelector
    )
{
    if (self == null)      throw new ArgumentNullException(nameof(self));
    if (xSelector == null) throw new ArgumentNullException(nameof(xSelector));
    if (ySelector == null) throw new ArgumentNullException(nameof(ySelector));

    return  self.GroupBy(xSelector)
            .ToDictionary(x => x.Key, xs => xs.ToDictionary(ySelector));
}

//--- キーを選択するだけでなく、要素として何を格納するかを選ぶバージョン
public static Dictionary<TKeyX, Dictionary<TKeyY, TElement>> ToDictionary2<TSource, TKeyX, TKeyY, TElement>
    (
        this IEnumerable<TSource> self,
        Func<TSource, TKeyX> xSelector,
        Func<TSource, TKeyY> ySelector,
        Func<TSource, TElement> elementSelector
    )
{
    if (self == null)            throw new ArgumentNullException(nameof(self));
    if (xSelector == null)       throw new ArgumentNullException(nameof(xSelector));
    if (ySelector == null)       throw new ArgumentNullException(nameof(ySelector));
    if (elementSelector == null) throw new ArgumentNullException(nameof(elementSelector));

    return  self.GroupBy(xSelector)
            .ToDictionary(x => x.Key, xs => xs.ToDictionary(ySelector, elementSelector));
}

使ってみよう

こんなものが一体どんなケースで役に立つというのだ…。と思うかもしれませんが、例えば以下のようなものがあります。

//--- こんなクラスがあるとする
class Block
{
    public X { get; set; }
    public Y { get; set; }
    public Color Color { get; set; }
}

//--- こういう 1 次元配列を 2 次元辞書化してしまえば...
var blocks = new []
{
    new Block{ X = 0, Y = 0, Color = Colors.Red },
    new Block{ X = 0, Y = 1, Color = Colors.Blue },
    new Block{ X = 1, Y = 0, Color = Colors.Green },
    new Block{ X = 1, Y = 1, Color = Colors.Yellow },
}
.ToDictionary2(x => x.X, x => Y);

//--- こんなに分かりやすくなる!
var color = blocks[0][1].Color;

…滅多に使わないとは思います(ハイ

*1:普通ならないと思う

2016 年を振り返って

ウチの CTO が毎年やっていて、「今年は僕も振り返ってみようかな」という気持ちになったので書いてみます。振り返りは大事だな、と言うことで!

単身赴任

2015 年 9 月に株式会社グラニに転職し、単身赴任生活が始まって早 1 年 3 ヵ月が過ぎました。転職して本当によかったって思っています。素晴らしいメンバーと毎日精一杯仕事ができているし、仕事ってこんなに楽しいんだなぁって社会人になって初めて思えました。...ただひとつを除いて。

それは家族と一緒にいる時間がものすごく少ないこと。これは単身赴任をしているので当然ですし、その選択をした自分のせいなのですが。家族って本当に大切だなぁとか愛しいなぁと改めて感じています。悲しませているとは思いつつ、自由にやらせてもらっていることに多大なる感謝が尽きません。だからこそ毎日何時間働いてもツラくないし、自分が選択したんだからって前向きになれます。そして帰省しているときは極力一緒に時間を過ごす、という (当然の?) 努力をするようになりました。単身赴任をするまでは「自分の時間をいかに確保するか」ばかりを考えていましたが、毎日が自分の時間になってしまった今はその逆ですね。以前にも増して仕事とプライベートにメリハリがついたのも良かった。

家族の理解とサポートって本当に素晴らしいですね。改めて、ありがとう。

どうして転職したの?

と、たまに聞かれます。「あんなに福井が好きって言ってたのに!」みたいな。すべてはコレだと思うんです。

プライベートではお友達 / 仕事では上司なこの方があまりにも、あまりにもブッ飛んでる。本当にリスペクトの極み。そんなスーパーな方の近くで仕事したかったから、というのが率直なところです。福井の田舎の会社で井の中の蛙にはなりたくなかった。そのためには環境を変えなきゃ / 冒険しなきゃって思えて、それで転職を決心しました。環境を変えるのが自分を変える最短かつ最高の方法だと思います。

もちろん、グラニ開発部のメンバー全員が素敵な個性と素晴らしい技量を持っていて日々尊敬していますし、毎日社内チャットでやりとりされる内容を見ているだけでレベルが高くて本当に勉強になります。

お仕事

入社してから 1 年ちょっと、グラニのメインタイトルである神獄のヴァルハラゲートの開発/運用をしてきました。日々の小さな改修はもちろんのこと、ユーザーさんが大いに盛り上がるイベントの実装までいろいろです。もちろんショックに沈む失敗もあったけれど、24 時間 365 日動き続けるゲームの運用の難しさや楽しさをいっぱい体験しました。ここで得た経験と自信はかなり大きい!

f:id:xin9le:20161231134015p:plain

夏にはグラニ開発部初のインターン生として @nanTosaka2 くんが 3 週間来てくださり、僕がメンターを担当していました。毎日「なんでこんなに優秀なの?」と目を丸くするばかりで、教えてもらうこともいっぱいありました。インターン終了後にブログを書いてくれたのを見て、ひとり泣いたのは本当です。とても有意義な時間を過ごせたみたいでこちらも感謝に絶えません。お疲れさまでした!本当にありがとね!

そして考え方や心境も結構変化しました。以前は「べき論」をかなり振りかざしていて、ちゃんとしていないことが大嫌いでした。それで喧嘩したり失いそうになったものもありました。そんなことにはもうなりたくないですし、そうしないための妥協を覚えた気がします。「人」をベースとした考え方の大切さ、忘れないようにしたいです。

C# 7

プライベートでは C# 7 のことばかり追いかけていて、重鎮 @ufcpp さんからお叱りを受けたりしつつ、新機能を先駆けて触ってみたのをベースにレビュー記事を書きまくっていました。社内で唱えていた言葉は「日本で最初に触ってみた系の記事を書く」でしたw そこだけは達成できたかなと思います。

勉強会でも C# 7 を唱え続け、C# 7 に関するセッションを半年以内で 3 回もやりました...(ぇ とは言えかなり好評で、de:code 2016 の直前にやったものが「de:code 2016 本体のセッションよりも詳しくてよかった」という評価もあって本当に嬉しかったです。C# 7 のリリースは楽しみにしています。

Hackathon

2015 年に引き続き 2016 年もハッカソンにちょこちょこ出たりしていました。SPA JAM 2016 というヤツでは東京の予選で最優秀賞を受賞して決勝に行ったりもしました。その様子はかなりの数のメディアで取り上げられ、素直に嬉しかったです。とは言え決勝ではしっかり負けたので、結果は何も残してないに等しいんですが!

紅白歌合戦の著作権表示

今日 12 月 31 日と言えば大晦日!大晦日と言えば日本中が楽しむ紅白歌合戦!そんな紅白歌合戦の公式アプリに Reactive Property が採用され、僕の名前も著作権表示に載りました

f:id:xin9le:20161231151710p:plain f:id:xin9le:20161231151716p:plain

「なんでお前がやねん!」というと、元々は @neuecc さんが作っていたライブラリなのですが、メンテが止まっていたので数年前から @okazuki さんと僕でメンテするようになりました。今は issue 対応のときの方針をどうするかを相談したり程度のただのお手伝いマンでしかないのですが、コントリビューターとして名前が入っているのでオマケとして載せていただいた感じです。これも嬉しい出来事でしたし、棚ぼたではありますが感謝に絶えません。

VR

「現在のお仕事は?」と言うと 2016 年 10 月からは新設された VR 部に異動し、その初期メンバーとして活動しています。VR 部発足の経緯などはプレスリリースが出ています。

f:id:xin9le:20161231140634p:plain

そんな Grani VR Studio 最初のプロダクトとして、弊社の六本木ヒルズオフィスを完全再現した Grani VR Office Tour を作成しました。Japan VR Summit 2 にもブース出展し、非常に良い評価をいただきました。TBS のあさチャン!でも取り上げてもらったりしました :)

f:id:xin9le:20161231141811j:plain

f:id:xin9le:20161231141819j:plain

と、最近は毎日 VR という新しい時代のためにメッチャ頑張ってます。けれど、まずまず僕自身が VR に出会ったのが 2016 年 4 月くらい。超新参者だし Unity も初心者なで毎日 ウーン...ウーン... と唸っていますが、日々新しいことにチャレンジできるのって本当に幸せなことだと思います。仲間と一緒にインパクトを残せるよう取り組んでいきます。2017 年はそんな全力の一年!

まとめ

この歳になると人のつながりの大切さに改めて気付かされます。そのためにも Team Geek にある HRT の精神は大切だなぁと痛感しますし、今後も大切にしていきたいと思っています。その上で来年はさらに加速していきます!今後とも、どうぞよろしくお願い致します。

そして、この 1 年に @neuecc さんと取締役で VR 部長の福永からいただいた金言を忘れないように。

モノを作るときに忘れちゃダメなこと 3 箇条

  • 作らない
  • 薄く作る
  • すぐ捨てる

未来はでっかく

  • 未来を作るのは、エンジニアのちょっとした創造力

ニンテンドーアカウントが Outlook.com で受信できないときの対処

やってますか?スーパーマリオ ラン!僕も少し嗜む程度にはやっています。

そんなスーパーマリオランはニンテンドーアカウントとの連携ができ、連携するといろいろ良いことがあります。

  • セーブデータが消えても大丈夫!
  • アイコンを Mii に設定できる
  • 連携しないと手に入らないキャラクターや建物が手に入る

とりあえずセーブデータがニンテンドーアカウント紐付きでサーバー側にバックアップされるので、機種変更してもデータの引き継ぎができます。これだけでも連携しておく価値が十分にあります。ということでニンテンドーアカウントを作ろうと決心しました。

認証メールが飛んでこない

ニンテンドーアカウントを新規に作ろうとすると登録フォームで入力したメールアドレスに対して認証コードの書かれたメールが飛んできます。しかし、待てど暮らせど認証メールが飛んできません。任天堂の Q&A で認証メールが届かない旨を調べてみると以下のようなことが書いてあります。

  • 入力したメールアドレスが間違っていないか
  • 迷惑メールフォルダに入っていないか
  • ご利用メールサービスの受信拒否設定で「no-reply@accounts.nintendo.com」が拒否設定になっていないか
  • 入力したメールアドレスがすでに別のアカウントに登録されていないか

最後は絶対にあり得ないし、その他も何度確認しても問題ありません。イライラが高まってググッてみると、Outlook.com / Office 365 のような Microsoft のメールサービスの場合は送信元がブラックリスト扱いされていると出てきました。

さすがの @aetos382 先生。僕も Outlook.com をメールアドレスとして設定しようとしていたのでダメでした。

ホワイトリストに入れよう

さて、Microsoft に問い合わせしてブラックリスト解除とかそんな高度で面倒で、本当にできるかも怪しいことに時間を割きたくはありません。かと言って Gmail で逃げるのも (できれば) 奥の手にしたい。と思ったところで強制的に受信許可設定すれば行けるのではないか?と閃き、トライしてみたら上手くいきました!

f:id:xin9le:20161224235612p:plain

上記のように、受信許可メーリングリストに「no-reply@accounts.nintendo.com」を追加すれば OK です。良かった ×2。

React ベースでもお構いなし!API を使わずに Instagram の投稿サマリーをスクレイピングで取得する

諸事情あって Instagram の投稿の情報を取得することになりました。とりあえず API 使うなんて超メンドクサイので、個人ページに表示されている投稿の情報をスクレイピングで取り出そうと考えます。簡単に言うと、以下のサムネイル部分の情報ですね。

f:id:xin9le:20161130012747p:plain

Instagram は React を使っている

こんなの超簡単だろうと思っていたらさすが Instagram。ページが React でできているようで、HTML を取得しても DOM のとして展開されていません *1。これは困った...。

とはいえ JavaScript で動的に展開しているだけなので、どこかに表示するのに利用する情報は隠れているでしょうとアタリは付きます。ということで HTML をジロジロと眺めていると script タグに JSON 形式でしっかり入っているじゃないですか!これを読み取れば何とかなる...!

f:id:xin9le:20161130014139p:plain

サンプルコード

先の JSON を読み込むサンプルをザッと書いてみました。今回は .NET Core ベースで書いているので、SgmlReader ではなく HtmlAgilityPack.NetCore を利用しています *2

//--- データ取得
public static async Task<InstagramPostSummary[]> GetInstagramPostSummaris(string userAccount)
{
    //--- HTML 取得
    var url = $"https://www.instagram.com/{userAccount}";
    var html = await GetHtmlAsync(url);

    //--- React でできているので DOM の直接解析はできない
    //--- データ部分の JSON を取得
    var targetText = "window._sharedData = ";
    var json = html.DocumentNode
             .SelectNodes(@"/html[1]/body[1]/script")
             .First(x => x.InnerText.StartsWith(targetText))
             .InnerText
             .TrimEnd(';')
             .Replace(targetText, string.Empty);

    //--- JSON を扱いやすい型に変換
    dynamic jObj = JsonConvert.DeserializeObject(json);
    JArray nodes = jObj.entry_data.ProfilePage[0].user.media.nodes;
    return nodes.Select(InstagramPostSummary.From).ToArray();
}

//--- HTML 取得
public static async Task<HtmlDocument> GetHtmlAsync(string url)
{
    using (var client = new HttpClient())
    using (var stream = await client.GetStreamAsync(url))
    {
        var doc = new HtmlDocument();
        doc.Load(stream);
        return doc;
    }
}

//--- データの入れ物になる型
public class InstagramPostSummary
{
    public long Id { get; private set; }
    public string Code { get; private set; }
    public long OwnerId { get; private set; }
    public DateTime Date { get; private set; }
    public string Caption { get; private set; }
    public int Width { get; private set; }
    public int Height { get; private set; }
    public string DisplaySource { get; private set; }
    public string ThumbnailSource { get; private set; }
    public bool CommentsDisabled { get; private set; }
    public int Comments { get; private set; }
    public int Likes { get; private set; }
    public bool IsVideo { get; private set; }
    public int? VideoViews { get; private set; }

    private InstagramPostSummary()
    {}

    public static InstagramPostSummary From(JToken token)
    {
        dynamic x = token;
        return new InstagramPostSummary
        {
            Id = (long)x.id,
            Code = (string)x.code,
            OwnerId = (long)x.owner.id,
            Date = new DateTime(1970, 1, 1) + TimeSpan.FromSeconds((double)x.date),
            Caption = (string)x.caption,
            Width = (int)x.dimensions.width,
            Height = (int)x.dimensions.height,
            DisplaySource = (string)x.display_src,
            ThumbnailSource = (string)x.thumbnail_src,
            CommentsDisabled = Convert.ToBoolean((string)x.comments_disabled),
            Comments = (int)x.comments.count,
            Likes = (int)x.likes.count,
            IsVideo = Convert.ToBoolean((string)x.is_video),
            VideoViews = (int?)x.video_views,
        };
    }
}

サイトによってやり方はまちまちですが、React を使ったサイトでもそれっぽくスクレイピングできました。とは言え、ちょっと意地になってしまった...。

*1:React は実行時に仮想 DOM から DOM を動的生成するため

*2:SgmlReader は投稿時点で .NET Core 対応が存在しないため

HTC Vive でコナミコマンド実装してみた

先日 Japan VR Summit 2 に向けて制作し、ブース展示した Grani VR Office Tour。その開発の一環でデバッグコマンドを入れていました。要件はこんな感じ。

  • 特殊なコマンド入力したらとある処理が発動
  • 普段の操作ではまず入力できない難易度
  • 覚えやすい

「それってコナミコマンドだよね!」ということで、HTC Vive コントローラーで動くコナミコマンドを実装しました。

f:id:xin9le:20161122164006p:plain

サンプルコード

実装は極めてシンプルで、UniRx を使えばチョチョイのチョイで終わります。以下がその実装例。なんとコレだけ!あとは発動時に実行したい任意の処理を書くだけです。

//--- こんな感じでボタンが定義されていたとして
public enum Buttons
{
    Top,
    Bottom,
    Left,
    Right,
    A,
    B,
}


public class KonamiCommand : MonoBehaviour
{
    [SerializeField]
    private GameObject controller;

    private void Start()
    {
        //--- コナミコマンドのボタン順序定義
        var buttonOrder = new []
        {
            Buttons.Top,
            Buttons.Top,
            Buttons.Bottom,
            Buttons.Bottom,
            Buttons.Left,
            Buttons.Right,
            Buttons.Left,
            Buttons.Right,
            Buttons.B,
            Buttons.A,
        };

        //--- UniRx を使って IObservable<T> 化したコントローラーのイベント
        var events = controller.GetComponent<ReactiveControllerEvents>();
        Observable.Merge
        (
            events.TouchpadTopPressedAsObservable().Select(_ => Buttons.Top),
            events.TouchpadBottomPressedAsObservable().Select(_ => Buttons.Bottom),
            events.TouchpadLeftPressedAsObservable().Select(_ => Buttons.Left),
            events.TouchpadRightPressedAsObservable().Select(_ => Buttons.Right),
            events.GripPressedAsObservable().Select(_ => Buttons.B),
            events.ApplicationMenuPressedAsObservable().Select(_ => Buttons.A)
        )
        .Buffer(buttonOrder.Length, 1)
        .Do(xs =>
        {
            //--- とりあえず確認のために console に表示
            var message = string.Join(", ", xs.Select(x => x.ToString()).ToArray());
            Debug.Log(message);
        })
        .Where(xs => buttonOrder.SequenceEqual(xs))  //--- 順序が一致していたら
        .Subscribe(_ =>
        {
            Debug.Log("コナミコマンド発動!");
            //--- do something
        })
        .AddTo(this);
    }
}

f:id:xin9le:20161122190532p:plain

当然ですが、Oculus Touch や他のデバイスでも同様にできます。Let's try!!