Elm でランダムにテキストを表示させる(その 2)

先日から作っていた web アプリが一応できた。

こういうスプレッドシートに文章を入力して、こういうサイトでランダムに表示させる、というもの。(何に使うものなのかは雰囲気で察してもらえればと)

Google スプレッドシートからは JSON でデータを吐くことができる(GET でのリクエストに対応するための関数が用意されている)ので、そちらは特筆すべきことなし。

下記記事のサンプルを参考にして、サクッと書いた。

変更したのはシート C 列 enabled のチェックがオフになっている行は除外する点と、選択したシートではなく全シートを対象とした点かな。

/**
 * Sheet から object にする
 * @param {Object} sheet
 * @param {Object}
 */
function convertFromSheet(sheet) {
    var rows = sheet.getDataRange().getValues();
    var keys = rows.splice(0, 1)[0];

    // filetering
    rows = rows.filter(function (row) {
        // enabled == true
        return row[2] === true;
    });

    var result = {};
    result["sheetName"] = sheet.getName();
    result["rows"] = rows.map(function (row) {
        var obj = {};
        row.map(function (item, index) {
            obj[String(keys[index])] = String(item);
        });
        return obj;
    });
    return result;
}

/**
 * 各シートのデータを filter した上で object にする
 * @returns {Object}
 */
function getObjectFromSheets() {
    const sheets = SpreadsheetApp.getActive().getSheets();
  return { sheets : sheets.map(convertFromSheet) };
}


/**
 * web app として公開する
 */
function doGet(e) {
    const data = getObjectFromSheets();
    return ContentService.createTextOutput(JSON.stringify(data, null, 2))
        .setMimeType(ContentService.MimeType.JSON);
}

で、Elm ですよ。

汎用的なプログラミング言語ではなく、Web のフロントエンドを作ることに特化した言語。Dart とかもそういう感じなのかな?知らないけれど。 コンパイルして html を出力したり、JS を出力したりできる。

# html
$ elm make src/Main.elm --output=index.html

# js
$ elm make src/Main.elm --output=elm.js

静的型付けで型の不一致をすぐ教えてくれるのがよい、ということを少し Twitter で書いたけれど、それに加えて、言語とフレームワークが一体となっていてプログラムの構成などで迷う必要がないのもいいところ。

細かいところについては各種ノウハウなどあるだろうけれど、大枠では Elm way に乗っかっていけばよくて、他の人も同じ構成でやっているからブログ記事なども参考にしやすかった。

OCamlHaskell に興味を持っていて少しだけかじったこともあった(歯型がついたかどうかくらいのレベルだけれど)ので、文法などはそんなに苦労せず理解できた。あと最近出たばかりの書籍があって丁寧に解説されていてわかりやすかったな。公式のガイド An Introduction to Elm も何回も読んだっけ。

基礎からわかる Elm

基礎からわかる Elm

あと、こうやってパイプライン演算子 |> を使ってすっきり書けると気分が良い。

addLineBreak : String -> List (Html Msg)
addLineBreak note =
    String.lines note
        |> List.map text
        |> List.intersperse (br [] [])

もう少し何か書こうと思っていたのだけれど、ひとまずこの辺で。

Elm でランダムにテキストを表示させる

所用で(?)シンプルな web アプリを作ることになり、Elm で書いてみることにした。

要件としてはルーレットのような感じで文章をシャッフルしてランダムに表示するもの。もう少し作り込む予定だけれどひとまず載せておこう。

とりあえずの感想

  • 見よう見まねで書いていると値は immutable ということを忘れがちだった。
  • エラーメッセージが親切なのでギリギリやっていけるという感じ。
  • バージョンが上がるごとに結構いろいろ API が変わっているようで、ブログ等の情報が参考にならない場合も多い印象。
  • update の処理を連鎖させるのはどうしたらいいのか、よくわからなかった。
    • 結局 Task.perform で繋ぐ感じでやっているが正しいのか不明。
  • 公式のフォーマッター elm-format があるので助かる。
    • でもインデントがずれていると動かなかったりするので Go の gofmt と比べると少々頼りない気がする。

もし、JavaScript で書いたとしたら、オンラインで公開されている参考にできるサンプルの数が多いので、おそらく半分くらいの時間でできたのではないかと思う。

まぁこうやって新しい概念に触れて、チャレンジしてみるのも大事だからね。

コード

Elm はシンタックスハイライトに対応していないか、そうか。

(190404 追記)
言語指定を Haskell にすることでそれっぽい感じでハイライトされるようになった。

module Main exposing (Model, Msg(..), init, main, subscriptions, update, view)

import Array
import Browser
import Html exposing (..)
import Html.Events exposing (..)
import Random
import Task
import Time



-- MAIN


main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }



-- MODEL


type alias Model =
    { sentences : List String
    , index : Int
    , sentence : String
    , shuffling : Bool
    , count : Int
    }


init : () -> ( Model, Cmd Msg )
init _ =
    ( { sentences = sampleSentences
      , index = 0
      , sentence = ""
      , shuffling = False
      , count = 0
      }
    , Cmd.none
    )



-- https://www.thetoptens.com/random-sentences/


sampleSentences : List String
sampleSentences =
    [ "I am so blue I'm greener than purple."
    , "I stepped on a Corn Flake, now I'm a Cereal Killer."
    , "Everyday a grape licks a friendly cow."
    , "Llamas eat sexy paper clips."
    , "Banana error."
    , "Don't touch my crayons, they can smell glue."
    , "There's a purple mushroom in my backyard, screaming Taco's!"
    ]


maxCount : Int
maxCount =
    20



-- UPDATE


type Msg
    = Pick
    | GetIndex Int
    | Tick Time.Posix
    | CheckCount Time.Posix
    | Shuffle Time.Posix


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Pick ->
            ( { model | shuffling = True }
            , Random.generate GetIndex (randomIndex model.sentences)
            )

        GetIndex i ->
            let
                s =
                    pickByIndex model.sentences i
            in
            ( { model | index = i, sentence = s }
            , Cmd.none
            )

        Tick newTime ->
            let
                c =
                    if model.shuffling then
                        model.count + 1

                    else
                        model.count
            in
            ( { model | count = c }
            , Task.perform CheckCount Time.now
            )

        CheckCount newTime ->
            ( resetStatus model
            , Task.perform Shuffle Time.now
            )

        Shuffle newTime ->
            ( shuffleSentence model
            , Cmd.none
            )


randomIndex : List String -> Random.Generator Int
randomIndex sentences =
    Random.int 0 <| List.length sentences - 1


pickByIndex : List String -> Int -> String
pickByIndex sentences index =
    let
        default =
            "this is default sentence."

        arr =
            Array.fromList sentences
    in
    Maybe.withDefault default (Array.get index arr)


resetStatus : Model -> Model
resetStatus model =
    if model.count == maxCount then
        let
            s =
                pickByIndex model.sentences model.index
        in
        { model | shuffling = False, count = 0, sentence = s }

    else
        model


shuffleSentence : Model -> Model
shuffleSentence model =
    let
        randomId =
            modBy (List.length model.sentences) model.count
    in
    let
        randomSentence =
            pickByIndex model.sentences randomId
    in
    if model.shuffling then
        { model | sentence = randomSentence }

    else
        model



-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg
subscriptions model =
    Time.every 40 Tick



-- VIEW


showBool : Bool -> String
showBool bool =
    if bool then
        "True"

    else
        "False"


view : Model -> Html Msg
view model =
    div []
        [ h1 [] [ text model.sentence ]
        , h2 [] [ text (String.fromInt model.count) ]
        , h2 [] [ text (showBool model.shuffling) ]
        , button [ onClick Pick ] [ text "Pick" ]
        ]

Ellie というオンライン開発環境(いわゆる playground 的なやつ)にも保存してみたけれど、表示されないかもしれない。

Elm の記事からヤギ画像を取り除く User Script を書いた

Elm とヤギ画像

Elm というプログラミング言語があって面白そうだなと思っているのだけれど、まぁ何というか、個人的にはアレだなーと思うところもあって。

ということで、User Script 書いてみた。

User Script

最近あまり聞かなくなった気がするけれど以前、Greasemonkey というものがあって、それの Chrome 版として Tampermonkey という拡張があった。

で、以下のスクリプトを登録することで目的を達成できた。

Qiita

// ==UserScript==
// @name         removeGoatQiita
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to remove images of goat
// @author       ryskosn
// @match        https://qiita.com/arowM/items/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    const imgs = document.getElementsByClassName('it-MdContent')[0].getElementsByTagName('img');
    for (const img of Array.from(imgs)) {
        img.parentNode.removeChild(img);
    }
})();

ちょっと雑かもしれないけれど、他のやり方が思いつかなかった。

elm-lang.jp

// ==UserScript==
// @name         removeGoatElmLangJp
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to remove images of goat
// @author       ryskosn
// @match        https://elm-lang.jp
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    const eyeCatch = document.getElementsByClassName('eyeCatch')[0];
    const moment = document.getElementsByClassName('moment')[0];
    eyeCatch.parentNode.removeChild(eyeCatch);
    moment.parentNode.removeChild(moment);
})();

それぞれのスクリプトを Tampermonkey のダッシュボードから設定する。

Image from Gyazo

190323 リカバリー中

2 月は自分のやるべきことに注力できていて、 結構いい感じで過ごせたのだけれど、3月に入ってから 低空飛行が続いている。

その要因として思い当たるものとしては、

  • 生活のリズムが少し変わった
  • イレギュラーなイベントも多々あった
  • 花粉症

といったところだろうか。

3/14 までは外部要因の影響が大きく、まぁこんなもんでしょうという 認識もあったが、その後の 1 週間もなかなか 2 月のようにはいかず、 現在リカバリーの真っ最中である。

どう考えても花粉症のせいだよね。まったくもう。

リカバリ

まぁ杉花粉に文句を言っても解決しないので、リカバリーについて考える。

何が問題かというと、2月と同じように行動できていないこと。 2月は守れていたルーティン、習慣がすっかりどこかに行ってしまった。

ここで悲観したり自己嫌悪に陥ったりするのはただの素人であり、 我々のようなもの(?)は、「あー、なるほどなるほど」と自身の状況 (続けていた習慣が途切れ、規律が乱れてしまっている現状)と、 それについてこれから対処しようとする自分を俯瞰して捉えるのだ。多分。

規律

まぁとにかく、復帰するためのアプローチとしては 「規律を取り戻すこと」を意識するのがいいのではないかと思う。

それも小さなところから少しずつでよくて、例えば、

  • 朝起きたらコップ半分の水を飲む
  • 脱いだ靴を揃える
  • 手洗い、うがいをする
  • 帰宅したら机の上にノートを開いて日付だけ書く

など、毎日続けられるようなことを意識して毎日続けていくのだ。

ある程度継続していれば、そのうちにそれをするのが当たり前になってきて、 都度意識する必要がなくなるので、空いた脳内メモリで他のちょっとしたことを やるようにするのがいいと思う。

規律と自己肯定感、あとは食事と睡眠、という感じで生活していれば 徐々に調子も上がってくるんじゃないかな。

OPAM、OCaml をアップデートする

OCaml コンパイラをインストール

まず opam 自体をアップデートする。パッケージマネージャは MacPorts を使っている。

$ sudo port upgrade opam

1.2 から 2.0 へメジャーバージョンアップということで多少コマンドが変わっているみたい。

opam switch list-available でインストール可能なコンパイラの一覧を表示する。

各種オプション適用版も含めて大量に表示されるのでコンパイラのバージョンだけを見るなら | grep base をつけてもいいかもしれない。

$ opam switch list-available
# or
$ opam switch list-available | grep base

最新のコンパイラ 4.07.1 をインストールしてみる。

$ opam switch create 4.07.1

バージョン 4.07 の変更点などはこちら。

各種パッケージをインストール

お好みで。

$ opam install -y utop
The following actions will be performed:
  ∗ install conf-m4     1          [required by ocamlfind]
  ∗ install ocamlbuild  0.14.0     [required by react]
  ∗ install dune        1.8.2      [required by utop]
  ∗ install ocamlfind   1.8.0      [required by utop]
  ∗ install jbuilder    transition [required by cppo, camomile, lambda-term]
  ∗ install base-bytes  base       [required by zed]
  ∗ install result      1.3        [required by lwt]
  ∗ install cppo        1.6.5      [required by utop]
  ∗ install camomile    1.0.1      [required by utop]
  ∗ install topkg       1.0.0      [required by react]
  ∗ install lwt         4.1.0      [required by utop]                        For the PPX, please install package lwt_ppx
  ∗ install react       1.2.1      [required by utop]
  ∗ install lwt_log     1.1.0      [required by lambda-term]
  ∗ install zed         1.6        [required by lambda-term]
  ∗ install lwt_react   1.1.1      [required by utop]
  ∗ install lambda-term 1.13       [required by utop]
  ∗ install utop        2.3.0
=====17 =====
$ opam install -y ppx_inline_test
The following actions will be performed:
  ∗ install sexplib0                v0.12.0 [required by base]
  ∗ install ocaml-compiler-libs     v0.11.0 [required by ppxlib]
  ∗ install ppx_derivers            1.0     [required by ppxlib]
  ∗ install base                    v0.12.0 [required by ppx_inline_test]
  ∗ install ocaml-migrate-parsetree 1.2.0   [required by ppxlib]
  ∗ install stdio                   v0.12.0 [required by ppxlib]
  ∗ install ppxlib                  0.5.0   [required by ppx_inline_test]
  ∗ install ppx_inline_test         v0.12.0
=====8 =====
$ opam install -y spotlib
The following actions will be performed:
  ∗ install ppx_tools_versioned 5.2.1 [required by ppxx]
  ∗ install seq                 base  [required by re]
  ∗ install ppxx                2.3.2 [required by ppx_test]
  ∗ install re                  1.8.0 [required by ppx_test]
  ∗ install ppx_test            1.6.0 [required by spotlib]
  ∗ install spotlib             4.0.3
=====6 =====
$ opam install -y ocp-indent
The following actions will be performed:
  ∗ install cmdliner   1.0.3 [required by ocp-indent]
  ∗ install ocp-indent 1.7.0
=====2 =====
$ opam install -y merlin
The following actions will be performed:
  ∗ install conf-which  1     [required by biniou]
  ∗ install easy-format 1.3.1 [required by yojson]
  ∗ install biniou      1.2.0 [required by yojson]
  ∗ install yojson      1.7.0 [required by merlin]
  ∗ install merlin      3.2.2
=====5 =====
$ opam install -y js_of_ocaml
The following actions will be performed:
  ∗ install uchar                0.0.2 [required by js_of_ocaml]
  ∗ install js_of_ocaml-compiler 3.3.0 [required by js_of_ocaml]
  ∗ install js_of_ocaml          3.3.0
=====3 =====

OCamlFormat というフォーマッタが開発中らしい。 gofmt みたいにカチッとフォーマットしてくれるのだとしたら嬉しいな。おって試してみるかも。

集中して仕事するクセ

いやー、何とか元気でやっています。
前回書いてから、もう 4 ヶ月くらい経ってるのか。早い。

最近の関心事としては、先週末くらいに以下のエントリを読んで、今さらながら「本当に集中しているか?」ということを考えるようになった。

いろいろあって、2 月からは 9 時から 18 時(休憩 1 時間)で、ほぼきっかり 8 時間勤務というスタイルでやっている。

それまでは遅くとも 20 時にはなるべく帰る、19 時台のときもあれば 20 時半とか 21 時を回ることもないわけじゃない、という感じだったので、2 月に入ってからは集中してやらないとすぐ 18 時になってしまうなーと感じてはいた。

でも、本当に集中してやっている時間がどれくらいあるだろうか、と自問するとどうだろう。

仕事の都合上、頻繁に割り込みタスクが発生して中断させられることがままあるという点を考慮したとしても、8 時間フルに集中してやっていると胸を張れるかというと難しい。(Toggl で時間を計ったりちょっとした工夫もしていたけれども)

そんなこんなで上記エントリを読んで、書かれているような 6 時間勤務の会社だと本当に集中してやらないと価値を発揮できないまま 1 日が終わってしまいそうだし、相応のプレッシャーも感じるだろうなと想像した。

ただ、

1 日 6 時間を継続して働き、成果を出し続けるというのは並大抵のことではない。ただこのことで会社に依存する時間をぐっとへらすことができるのは事実だし、6 時間という時間の使い方が慣れていく。

そして、

毎日集中して働き続けるはある意味、修行に近いと思う。ただそれになれることで集中して仕事するクセを付けられるのではないか?という考えに基づいている。

このようにも書かれていて、いずれも全くもってそのとおりだと思う。

最初はプレッシャーがあったり、しんどさを感じるかもしれないけれども、続けていくうちに「集中する筋力」が鍛えられていき、やがてはそれがその人にとっては普通の働き方になっていくのではないか。

頭は使わないと悪くなる一方だというし、文章も日頃から書いていないとスピードが落ちる。

同様に、ものごとに集中して取り組むスキル(?)というのも鍛えれば向上するかもしれないし、鍛えなければ緩やかに低下していく一方なのだとしたら、これは無視できないなーと思っている。

会社の制度も違うし、そのまま同じようにできるかというとそうもいかないかもしれないけれど、できる範囲で集中して仕事するクセをつけられるよう、取り組んでいきたい。

(早速月曜から意識してやってみているんだけどなかなかハードだ)

ホテル宿泊料金の変動について

それはどうなんだろう、と思ったことがあったので記録として残しておく。

澤氏の tweet から

他者の反応など

その 1

その 2

その 3

思ったこと

  • ホテル名称明記ヤクザ呼ばわりがなければ、ここまで引っかからなかっただろう。
  • 繁忙期やイベント時に高額な設定となることに対して「こっちの足元見やがって、チクショウ!」というような腹立たしい気持ちになったとしたら、それもしょうがないとは思う。
  • しかし、今回はホテル側が一方的に悪い話なのかというと、そうではないと考えている。

前提の推測

いずれも推測なので外れているかもしれない。

1. 宿泊予約を手配するタイミングが遅かった(と思われる)

  • 著しく高額に設定された部屋(今回の部屋)しか選択肢がない状況にあったらしいという点から。

2. 澤氏(が依頼したダイナースの担当者)の前にも当該ホテルを検討したユーザーはいた(と思われる)

  • 当該ホテルが料金設定を変更してから、澤氏(が依頼したダイナースの担当者)がアクセスするまでの間、同じ日程で宿泊施設を探している他のユーザーからも当該ホテルへのアクセスはあっただろう。
  • 上記の通りタイミングが遅かったと思われるので、その間、他のユーザーが当該ホテルに全くアクセスしていなかったとは考えにくい。

3. 「ホテルに宿泊する」以外の選択肢もあった(と思われる)

  • 福岡市内であればカプセルホテルや漫画喫茶などの、よりカジュアルな施設も利用可能だったのではないか。

もし著しく高額な料金設定ではなかったとしたら

  • 澤氏(ダイナースは省略)が宿泊予約を手配しようとした時点で、すでに他のユーザーがその部屋を確保して埋まっていたのではないだろうか。

言い換えると、ホテル側が著しく高額に設定していたからこそ、その料金設定のおかげで、(その金額を支払うことは難しい)他のユーザーは諦めざるを得ず、その金額でも払おうと思えば払える澤氏がアクセスするまで空室が残っていた、とも言えるだろう。

元の tweet へリプライしている方々も「そのホテルは酷いですね」という反応だけれど、高額な料金を払うくらいなら埋まっていた方がマシだったと考えるのだろうか?

だから「市場価格から大きく乖離している≒やり過ぎ」が気に入らないというのであれば、その空き室も埋まっていたものとして泊まらなければよかったのではないかと思う。

これは「相対取引なのだから文句があるなら買うな」とか「双方合意の上なのだから後から文句言うな」という話とほとんど同じようだけれど、部分的に異なるのではないかと個人的には思っている。

価値について

再掲

通常 10,000 円程の部屋が 48,000 円で提供されたことを指して「価値と見合わない価格設定を平気でしちゃって」と書いている。

これを読んで澤氏はホテルの施設・サービス等だけを「価値」の評価対象としているような印象を受けた。

このホテルが提供した価値はそれだけだというのは、何だか「原価はこれだけなのに販売価格が高い!」と言い立てる人と似通っている部分があるように感じるのは私だけだろうか。

  • 11 月下旬の 3 連休
  • 人気アーティストのコンサート x 2
  • 大規模な学会

といったイベントが重なり大混雑が予想される状況の中で、最低限安全で清潔で独立した個室で宿泊できるという部分にも、目に見えない価値が含まれていると思う。現に澤氏は漫画喫茶や野宿等ではなくそのホテルで宿泊することを選択したわけで。

「○○という状況で▲▲を提供する」という形で▲▲が通常時よりも高額な設定になるというのは観光地や、海とか山でもよくある話。

まぁ、提供価値に比べて値段が高い、という意見自体を否定するつもりはない。感じ方は人それぞれだし、感じたことを tweet するのも個人の自由だ。

ダイナース

すごそう。

再掲

ダイナースは他のカード会社・ホテルのコンシェルと比べて店舗側の手続きが煩雑で尚且つ手数料が高いので、ぼったくりたくなる衝動に駆られますね。

という店舗側の視点からのお話があった。

ダイナースがカード利用者へ還元している分の原資はどこからやってくるのだろうか。

結論

繰り返しになるけれども、48,000 円で申し込まざるを得なかったような人は、仮に当該ホテルがもう少し穏当な料金設定をしていたとしたら空室にありつくことはできなかったのではないだろうか。

(需要 > 供給の状況下で、手配のタイミングが遅めだったから)

もし、

  • 漫画喫茶や野宿ではなくホテルに泊まりたい
  • 高額な料金を請求されるのは許せない(請求するホテルが悪い)

というのであれば、早めに手配するというユーザー側で取りうる対策があったと思う。

澤氏もリプライしていた諸氏も、まさか知らなかったわけではないだろう。もちろん、その対策を取らなかった、取れなかったのはホテル側の責任ではないはず。

それもわかっていながら、ホテル名称を明記して

  • 「ぼったくり」
  • 「たぶんここヤクザが経営してると思うw」

と発言するというのはどうなんだろう。

仮にこれが、申し込み時には 10,000 円と聞いていたのに会計時に 48,000 円を請求されたとか、そういう話であれば上記の発言もわからなくもないけれど、そうではないよね?

1 ユーザーの体験としての話、個人の感想は全く否定しませんが、自身の責任を棚上げして一方的に誹謗しちゃってこの先いいことあるんかいな、というのがポイントでございました。

「言い方」大事ですねー!

(正直なところ、これは自分の認識や考え方がどこかおかしいのだろうか、という思いを拭い去ることができず、こうして書き連ねてしまった次第)

参考

混雑の原因

大きなイベントが重なっていたようだ。

料金の変動について

追記

2018-11-25 13:09 追記

私もこういう料金変動と無縁ではない。今年も例に漏れず出遅れた。

同日 13:40 追記

当社では平成19年12月より、MSNトラベルのツアー情報ページをマイクロソフト社から全面移管し、運営しております。大手旅行会社から中小、中堅旅行会社まで幅広く営業し、Webでのツアー販売のサポートをさせていただいております。

2010年 06月 01日

現在は関わっていないのかもしれないけれど。

 とはいえ、相手の意見や主張が明らかに理不尽だと感じたり、自分の不利益が大きくなるような事象に直面したりした場合には、自分が怒っていることを相手に伝える必要があります。その時に必要なのは、「事実を具体的かつ明確に伝え、自分の所感とは分けておく」という考え方です。

 怒っていると、自分の感情を中心にものを言いがちですが、まず大事なのは事実の共有です。

気をつけたい。

台湾に行ってきた

先日 2 泊 3 日で台湾に行ってきた。

以前 8 月くらいに彼女の台湾の友人が仕事で日本に来て、僕も一緒に食事したことがあった。そのことを思い出して、いつか行きたいね〜などと話していた際にちょっと調べてみようかと航空券の価格を確認したら案外安かったので、週末にサクッと行こうということになった次第。

LCC(Jetstar)とはいえ往復で 18,000 円くらい、ホテルが 2 泊で 7,000 円くらいだった。かなり安いね。

月曜、火曜と有給を利用して、土曜 -> 火曜の 3 泊で検討していたけれど土曜発だと結構価格が変わるのと、2 泊でもいいかという話になって、土曜の夜に成田を出て深夜 1 時くらいに台北の桃園(タオユエン)空港に到着、日曜、月曜を台北で過ごして、月曜の深夜 2 時くらいの便で発って火曜の早朝に成田着、というスケジュールだった。

感想

言葉

駅やデパート、タクシーなどの観光客と接する人たちはだいたい英語が通じるっぽい。それ以外のお店でも、英語か日本語が話せる人が一人はいるという印象だった。

茶器のお店や、お洒落なカフェの店員さんはかなり流暢に日本語で応対してくれたのでびっくりした。それだけ日本人のお客さんも多いんだろうな。

あと公園の広場みたいなところで掃除係のおばさんに「日本人?」って話しかけられて、そうですと答えたら

昔 1 年だけ大阪に行っていたことがある、私は日本が大好き!日本人はホンマ世界一!
(中略)
韓国出身なんやけど兄弟はみんなアメリカにおる。日本人は本当に一番や!
この辺は天王寺みたいなもんでガラ悪いから気ぃつけてや!

と勢いよくブワーっと話し始めたので、びっくりした。と同時に、そういうふうに思ってくれているというのはありがたいことだと感じた。

物価

  • 通貨はニュー台湾ドル(TWD)で、1 TWD が 3.7 円くらい。
  • 電車の運賃が安い。普通に 5、6 駅移動しても 20 TWD とか。
  • 食事もだいたい安いと思う。夜市のカジュアルなお店ではご飯ものが 50 TWD くらい、他のメニューも 100 TWDとかだった。

wi-fi

空港到着が深夜だったので通信会社の店舗は閉まっていたのと、公共 wi-fi があるというからそれで済まそうとしたが、ちょっと苦しかった。

公共 wi-fi はあまり繋がらなくてほぼ役に立たなかった印象。旅慣れていない素人は素直に SIM を買って入れるべきだった。

観てきたところ

龍山寺

多くの仏様、神様(道教)が祀られているメジャーなお寺。お参りの作法も日本とは異なるので見よう見まねでやってみた。長いお線香に火をつけてそれを持ちながら歩き回るので気が抜けない。

建物の柱や天井などの装飾もとても凝っていて綺麗だった。10 年以上前だけれどタイに行ったときにバンコク市内の寺院にて細かい装飾を見て感嘆したっけ。

日本、タイ、台湾と仏教というくくりでは同じだけれど様式は結構違いがあるね。大乗仏教小乗仏教とかそういう話だっけ?今度調べてみるかも。

迪化街

昔の建物が残る問屋街。新し目の小洒落たお店もあり、レトロな雰囲気もあり、で面白かった。好きな人は好きだと思う。

夜市

日本でいうお祭りの夜店と屋台と商店街が合わさっている感じ。人が多く、ごった返しているので、移動にも気を使う。

観光客だけでなく、若者グループや家族連れなどローカルの人たちも利用しているようだった。

故宮博物院

台湾の国立博物館。敷地が大きくいくつか建物があるようだった。今回見たのは本館のみ。館内が基本的に撮影 OK でみんな写真撮ってた。

HTC VIVE とのコラボレーション(?)の展示があった。いくつか種類があって、時間がなかったので試さなかったけれど係の人の PC を覗いた限りではいずれもかなり綺麗な映像で面白そうだった。

中正紀念堂

「中正」とは中華民国初代総統であった「蒋介石」の本名(=蒋中正)を指し、1975 年 4 月 5 日に亡くなった蒋介石に対する哀悼の意を込めて建てられました。

まぁまぁ穏やか

しばらく書いていなかったので書き方を忘れてしまった。
もう 10 月も終わりそうなの?早いね。

最近は仕事ではコードをほとんど書かなくなった。 自動化したいタスクはいくつかあれど、そのためにプログラムを書く時間がない。まぁしょうがないよね。

プライベートでは 8 月頃に少しだけ書いてた。特定のアプリケーションにキー入力を送って、キャプチャを撮って、Gyazo にアップロードして esa に書き込むという感じ。

PythonPyAutoGUI というライブラリを使ってみたらなかなか便利だった。

動かしているプログラムから一部抜き出すとこんな感じ。

import pyautogui as pag

pag.PAUSE = 0.8

class Captor:
    def activate_by_image(self, filepath: str) -> None:
        """search image on screen and activate application"""
        location = pag.locateOnScreen(filepath, grayscale=True)
        if location:
            x, y = pag.center(location)
            pag.click(x, y)
        else:
            print('image missing on screen...')
            return

既存の画像との照合ができるので普通の GUI アプリの自動化にも使えるよ。

やらなきゃいけないことはたくさんあるから全然気を抜けないし、だらけている暇はないけれど、その割にはまぁ何とか一応は穏やかに過ごせているかな。

失効しそうな年休が 10 日以上残っていたので期限までに利用したい旨を伝えたら、何だかんだで繰り越して OK ということになったのでしばらく水曜を休みにして週休 3 日生活を送ろうと考えている。

メールが来ていないことを通知する

朝、以下の tweet を見かけて面白そうだったので考えてみた。

IFTTT はアカウントは作ったものの、ほとんど使っていなかったな。 活用したら便利なのかも。

で、軽く触ってみた限りでは IFTTT の機能だけでは上記を実現するのは難しそう。

来るべきメールが来ていないことを知るには、最後にそのメールが来た時点からの経過時間を所定の閾値と比較すればよいのではないだろうか、と考えて、頭の体操がてら、Google Apps Script で書いてみた。

Google ドライブ新規 -> その他 -> Google Apps Script を選択して、下記コードを貼り付けて保存すると動かせると思う。

// Gmail の検索クエリ
var query = 'newer_than:1d 何とかの通知';

// 何分以上前かの閾値
var threshold = 120;

// 結果の通知を送るメールアドレス
var adminAddress = 'test@example.com';


/**
 * エントリポイント
 * この関数にトリガーを設定して毎日実行する
 * 
 * 例えば、通常 8:00-9:00 の間に通知メールが来るのであれば、
 * トリガーを「日タイマー: 9:00〜10:00」に、 threshold を 120 に設定すれば
 * 来るべきメールが来ていないことを検知できるのではないだろうか。
 */
function main() {
  var d = getLatestMailDate(query);
  if (isLongerPeriod(d, threshold)) {
    sendNotificationMail();
  }
}

/**
 * 指定したアドレスへメールを送る
 */
function sendNotificationMail() {
  var subject = '直近 ' + threshold + ' 分以内にメールはありませんでした。(クエリ: ' + query + ' )';
  var body = 'Hello world!';
  GmailApp.sendEmail(adminAddress, subject, body);
}

/**
 * クエリで検索したメールのスレッドから最新のメールを取り出し、日時を取得する
 * @param {String} q - 検索クエリ文字列
 * @return {Date}
 */
function getLatestMailDate(q) {
  var threads = GmailApp.search(q);
  var latestThread = threads[0];
  var messages = latestThread.getMessages();

  // 配列の最後の要素(メール)  
  var latestMessage = messages[messages.length - 1];
  var d = latestMessage.getDate();
  return d;
}

/**
 * 実行時の現在時刻との差分が指定値よりも大きいか比較する
 * @param {Date} d - 日時オブジェクト
 * @param {Number} t - 何分以上前か
 * @return {Boolean}
 */
function isLongerPeriod(d, t) {
  var now = new Date();

  // 現在時刻との差分(単位はミリ秒)
  var diff = now.getTime() - d.getTime();
 
  // 分に換算
  var minutes = diff / (1000 * 60);
  return minutes > t;
}

あとはこの通知メールの受信を IFTTT で LINE に通知すればよいのではないか。たぶん。

初回実行時の手順

GAS から Gmail にアクセスするために初回実行時には認証ダイアログが表示される。

GAS のトリガー設定

毎日繰り返し実行する場合、実行タイミングは 1 時間の幅を持たせて指定する形になる。

https://gyazo.com/9fb8af857d229de347bbf33a88c5ddb6