20190417のJavaScriptに関する記事は30件です。

【JavaScript】if ( value == false ) と if ( value != true ) の違いを答えよ

テスト用コード

検証モードに以下のコードを貼り付けて、テスト。

function test( value ) {
  if ( value == false ) {
    console.log('value == false です');
  }
  if ( value != true ) {
    console.log('value != true です');
  }
}

test(true) など値をいろいろ入れて見る。

答え

JavaScriptは変数宣言がないため、valueにくる値がboolean型とは限らない。つまり
if ( value == false ) は「valueがfalseのとき」
if ( value != true ) は「valueがtrue以外の値のとき(false以外の値でも)」

例えば

test('a')
→ value != true です

test([])
→ value == false です
value != true です

test(['a'])
→ value != true です

test(undefined)
→ value != true です

まとめ

if文を書くときは注意。
変数宣言だけして値をいれてない変数がある場合は値が undefined となるため、上記のif文の使い分けをして扱うことができる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

声優名で話しかけると出演するゲームを教えてくれるLINE BOTを作ったったー

sample_screen.png

友達登録してくれると喜びます!!

友だち追加 qr_code.png

どんなBOTなの?

声優名で話しかけると出演するゲームを5件まで教えてくれます!!
話しかけ方として以下の3つがあります。

  • 声優名
  • 先月の声優名
  • 来月の声優名

例えば「風音」さんの出演しているゲームを調べたい時はBOTに対して

  • 風音
  • 先月の風音
  • 来月の風音

と話しかけます。

Screenshot_20190412-093431.png

実行タイミングが4月12日(金)なので風音の場合は2019年4月に発売するゲームを教えてくれ、同様に先月の風音の場合は2019年3月で来月の風音の場合は2019年5月の出演するゲームを教えてくれます。

出演するゲームがない場合は出演する予定はありませんと教えてくれます。

声優名以外にも「リスト」と話しかけるとゲームの発売リストページを教えてくれます。
話しかけ方は声優名と同じで以下の3つがあります。

  • リスト
  • 先月のリスト
  • 来月のリスト

sample_list_screen.png

仕組みは?

Ruby(スクレイピングツール)とGASを使用して作成しています。

ざっくりとした図は下記のとおりです。

プログラム詳細.001.jpeg

スクレイピングツール

げっちゅ屋発売日リストとゲームの紹介ページをスクレイピングしその結果をコマンドラインにて表示、絞り込みできるツールになります。

sample.gif
https://github.com/dodonki1223/eroge_release_cmd

このスクレイピングツールでゲームの発売リスト情報を取得しCSVに出力します。

出力したCSVの内容を元にGoogleスプレッドシートへ書き込みを行います。

Googleスプレッドシート

GAS

https://github.com/dodonki1223/eroge_release_bot

GASはLINE BOTと連携してLINE BOTからの入力を受けとり、その内容に合致したものをLINE BOTに返す役割をしています。

作成理由は?

Rubyの勉強のため、Rubyを使って何か作成しようと思い、作ったのがこのLINE BOTです。最初はRubyによるスクレイピングツールだけのつもりだったのですがどうせならLINE BOTも作ってしまえってことで作成しました(笑)
これを作成する前はRuby初心者です。一度もRubyを書いたことありませんでした。

スクレイピングツールの開発について

今回作成したスクレイピングツールのソースはこちら

基本的なRuby構文を書く時に参考にしたこと

公式サイトこそ正義だと思っているので、必ず公式サイトのリファレンスを読んでプログラムを書いていきました。
公式サイトの内容がよくわからない時はブログやQiitaの記事を参考に開発しました。
rubocopというLinterツールのgemを入れ、コードは基本的にコイツに監視してもらい、その他でどう書いたらいいのだろうかというところはCookpadのコーディング規約を参考にしました。

RSpecを書く時に参考にしたこと

今まで働いてきたところではテストコードを書く文化に触れたことがなかったので勉強のために特に力を入れたのがRspecです。
RSpecの独特な記述に苦しまれつつ、書いていました……。慣れてくるといいものですね。
先程でも述べたように公式サイトこそ正義なので必ず公式サイトを読むようにしました。

Better SpecsというRSpecのベスト・プラクティスを教えてくれるサイトを特に参考にしました。ただこのBetter Specsは難しいことはあまり書かれていなくて基本的なことが多い印象です。

応用的な書き方はrubocop内のRSpecを参考にしました。
Mockの作り方やコードの簡略化にすごく参考になりました。

少し古い記事ですが、Mockの考え方は@jnchitoさんの記事を参考にしました。すごくわかりやすくまとめてあり勉強になりました。

スクレイピング

スクレイピングはNokogiriを使用して行いました。スクレイピング関連の記事は探せばいろいろと出てくるのですが、スクレイピングのRSpecコードのサンプルが無くて困りました。
なので今回はスクレイピングのやり方については特に説明しません。興味がある人はGithubのソースを見て下さい。

スクレイピングのRSpecコードで重要だと思ったのはサイトの構造が変わり、値が取得できなくなることだと思ったので、テストは最小限に値が取得できるかどうかをテストしているだけです。

サイト構造が変わったら結局スクレイピングコードを作り直さないといけないと思うので、これぐらいで良いと思いました。

    let(:target_year_month) { '201902' }
    let!(:release_list_scraping) { described_class.new(target_year_month) }

    describe '#scraping' do
      let(:release_list) do
        VCR.use_cassette 'release_list' do
          release_list_scraping.scraping
        end
      end
      let(:first_elemet) { release_list[0] }

      it { expect(first_elemet).to include(:release_date, :title, :introduction_page, :id, :brand_name, :price) }

      it { expect(first_elemet[:release_date]).not_to eq '' }
      it { expect(first_elemet[:title]).not_to eq '' }
      it { expect(first_elemet[:introduction_page]).not_to eq '' }
      it { expect(first_elemet[:id]).not_to eq '' }
      it { expect(first_elemet[:brand_name]).not_to eq '' }
      it { expect(first_elemet[:price]).not_to eq '' }
    end
  end

VCRというRSpecのWebmockを簡単に作成できるgemを使用しています。「VCRを使うとRSpecのWebmockの作成が超絶楽になった! | 酒と涙とRubyとRailsと」のサイトで、ものすごくわかりやすく説明してくれているので、見てみて下さい。

1回目は時間がかかりますが、2回目以降はVCRのおかげで高速でテストが実行できます。

コマンドライン引数

VB.NETでコマンドライン引数を制御するプログラムを作成していた時は自作していたのですが、Rubyだと簡単にコマンドライン引数を制御することができるクラスがあったのですごく助かりました。
この辺を自分で実装するとすごくめんどくさい記憶があるので簡単に実装できて驚きました。下記の記事がものすごく參考になりました。

  # コマンドライン引数クラス
  #   コマンドから受け取ったコマンドライン引数をパースして
  #   プログラムから扱えるようにする機能を提供する
  class CommandLineArg
    attr_accessor :options

    # コンストラクタ
    #   コマンドライン引数を受け取れるキーワードの設定、ヘルプコマンドが実行
    #   された時のメッセージの設定
    #   コマンドライン引数の値を取得するようのHash変数の作成
    def initialize
      # コマンドライン引数の値をセットするHash変数
      @options = {}
      OptionParser.new do |opt|
        # ヘルプコマンドを設定
        opt.on('-h', '--help', 'Show this help') do
          puts opt
          exit
        end

        # げっちゅ屋のrobots.txtの内容を表示するコマンドを設定
        opt.on('--robots', 'Display contents of robots.txt') do
          puts GetchuyaScraping.robots
          exit
        end

        # 値を受け取る系のコマンドライン引数を設定する
        opt.on('-y', '--year_month [YEAR_MONTH]', 'Set Target Year And Month') { |v| set_command_line_arg_value(v, :year_month, '年月') }
        opt.on('-v', '--voice_actor [VOICE_ACTOR]', 'Narrow down by voice actor name') { |v| set_command_line_arg_value(v, :voice_actor, '声優名') }
        opt.on('-t', '--title [TITLE]', 'Filter by title') { |v| set_command_line_arg_value(v, :title, 'タイトル名') }
        opt.on('-b', '--brand_name [BRAND_NAME]', 'Narrow down by brand_name') { |v| set_command_line_arg_value(v, :brand_name, 'ブランド名') }

        # true、falseを受け取るコマンドライン引数を設定する
        # デフォルト値はすべてfalseとし、受け取ったものにはtrueをセットする
        @options[:csv]              = false
        @options[:json]             = false
        @options[:open]             = false
        @options[:spreadsheet]      = false
        @options[:open_spreadsheet] = false
        @options[:clear_cache]      = false
        @options[:simple]           = false
        opt.on('-o', '--open [OPEN]', 'Open game page in browser') { @options[:open] = true }
        opt.on('-c', '--csv [CSV]', 'Create a csv file') { @options[:csv] = true }
        opt.on('-j', '--json [JSON]', 'Create a json file') { @options[:json] = true }
        opt.on('-s', '--spreadsheet [SPREADSHEET]', 'Write to spreadsheet from CSV') { @options[:spreadsheet] = true }
        opt.on('--open_spreadsheet [OPEN_SPREADSHEET]', 'Open spreadsheet page in browser') { @options[:open_spreadsheet] = true }
        opt.on('--clear_cache [CLEAR_CACHE]', 'Clear the cache') { @options[:clear_cache] = true }
        opt.on('--simple [SIMPLE]', 'Display results in a simplified way') { @options[:simple] = true }

        # コマンドラインをparseする
        opt.parse!(ARGV)
      end
    end

    # 対象のコマンドライン引数が存在するか?
    def has?(name)
      @options.include?(name)
    end

    # 対象のコマンドライン引数の値を取得する
    #   対象のコマンドライン引数の値が存在しない場合は空文字を返す
    def get(name)
      return '' unless has?(name)

      @options[name]
    end

    private

    # コマンドライン引数をインスタンス変数にセットする
    #   もし対象のコマンドライン引数がnilの時はメッセージを表示して処理を終了する
    def set_command_line_arg_value(value, key, param_name)
      if value.nil?
        puts "#{param_name}のパラメータを指定して下さい"
        exit
      end
      # 配列かどうかを取得し、配列の時は配列をセットそうでない時は値をそのままセットする
      is_array = value.split(',').count > 1
      set_value = is_array ? value.split(',') : value
      @options[key] = set_value
    end
  end

Googleスプレッドシートの操作

Googleスプレッドシートのシートを削除したり、新規に作成したりするのにgoogle-drive-rubyというgemを使用しました。
使い方についてはドキュメント通りなのですが、ドキュメント以外のことをやろうと思った時、あまりサンプルがなかったのでGithubのソースを見てどんなことができるのか確認しながら作成しました。

# コンストラクタ
#   IDを引数で受け取り、もしIDが存在しなかった場合はGoogleスプレッドシートが見つかりません例外が
#   発生する
#   ※必ず存在するGoogleスプレッドシートがあること前提です
def initialize(spreadsheet_id)
  # jsonファイルのconfigファイルからGoogleDrive::Sessionを作成する
  # https://github.com/gimite/google-drive-ruby/blob/master/doc/authorization.mdに
  # google_drive_config.jsonファイル作成方法が書かれています
  @session = GoogleDrive::Session.from_config(CONFIG_FILE_PATH)
  begin
    @spreadsheet = @session.spreadsheet_by_key(spreadsheet_id)
  rescue Google::Apis::ClientError => e
    puts "指定されたIDのスプレッドシートが見つかりませんでした\n#{e.message}(#{e.class})"
    raise
  end
end
# @spreadsheetは「GoogleDrive::Spreadsheet」です

# Googleスプレッドシートのワークシートをタイトルから削除する
#   ワークシートが見つからなかった場合はメッセージを表示
def delete_worksheet_by_title(title)
  # 削除対象のワークシートを取得
  target_worksheet = @spreadsheet.worksheet_by_title(title)

  # 削除対象のワークシートが見つからなかった場合はメッセージを表示して処理を終了する
  if target_worksheet.nil?
    puts "ワークシート名が「#{title}」のワークシートが見つかりませんでした"
    puts 'ワークシートの削除が出来ませんでした'
    return
  end

  # ワークシートの削除処理を実行
  target_worksheet.delete
end
# @spreadsheetは「GoogleDrive::Spreadsheet」です

# Googleスプレッドシートのワークシートをタイトルから取得する
#   ワークシートが存在しない時は作成したワークシートを@worksheetにセットする存在する時はそのワー
#   クシートを@worksheetにセットする
def get_worksheet_by_title_not_exist_create(title)
  @worksheet = @spreadsheet.worksheet_by_title(title).nil? ? @spreadsheet.add_worksheet(title) : @spreadsheet.worksheet_by_title(title)
  @worksheet
end
@worksheetは「GoogleDrive::Worksheet」です

# GoogleスプレッドシートへCSVファイルから書き込みを行なう
def write_by_csv(file_path)
  begin
    # CSVファイルの読み込み
    csv_data = CSV.read(file_path)
  rescue StandardError => e
    # 例外メッセージとバックトレースを表示して処理を終了する
    puts "CSVファイルが見つかりませんでした\n#{e.message}(#{e.class})"
    raise
  end
  # CSVファイルからデータを取得し、スプレッドシートへ書き込む
  csv_data.each_with_index do |data, row|
    data.each_with_index do |value, cell|
    # google-drive-rubyの仕様で1行目は1から、1セル目は1からなので行とセルにそれぞれ+1する
    @worksheet[row + 1, cell + 1] = value
    end
  end
  @worksheet.save
end

google-drive-rubyのコードのRSpecはMockを多用して書きました。
量が多いのでリンクだけ貼っておきます。興味がある人は読んで見て下さい。

LINE BOTの開発について

LINE BOT

LINEの社員さんが書かれているQiitaの記事を參考にLINEの設定を行いました。
Node.jsのプログラム開発以外はこちらを參考にしました。

LINE BOTとGASとの連携

下記記事を參考にしました。

LINE BOTとGASの連携は本当に簡単です。1時間もあれば簡単に連携することができました。

LINE Developersの設定でWebhook送信利用するにし忘れることがあります。
利用するにしてないとLINEから文字を入力してもうんともすんとも反応しないので必ず利用するになっているか確認して下さい。

LineImage

GASについて

今回のGASの役割はLINEからの入力を受け取り、受け取った内容でスプレッドシート内を検索し合致した内容をLINEに返すことをしています。

今回作成したGASのソースはこちら

LINEからPOSTされた時に実行される処理

GASをWEBアプリケーションとして公開しPOSTを受け取る時はdoPost(e)を定義しなくてはいけません。その辺のことはWeb Apps  |  Apps Script  |  Google Developersに書いてあるので読んでみると良いかもしれないです。
つまりLINE BOTを作るだけならdoPost(e)メソッドだけ用意してあげればOKです。
今回作成したBOTのdoPost(e)メソッドの中身を紹介していきます。

function doPost(e) {
  // LineBotからPostされたデータを取得
  // Webhookイベントオブジェクトの返す値について
  // →https://developers.line.biz/ja/reference/messaging-api/#webhook-event-objects
  var replyToken  = JSON.parse(e.postData.contents).events[0].replyToken,  // WebHookで受信した応答用Token
      userMessage = JSON.parse(e.postData.contents).events[0].message.text; // ユーザーのメッセージを取得(声優名)

  // 対象の月を取得する
  var targetMonth = getTargetMonth(userMessage);

  // 対象のシートを取得
  var yearMonth = getYearMonth(targetMonth),
      sheet     = getSheet(yearMonth);

  // 声優名を取得する
  var voiceActorName = getVoiceActorName(userMessage)

  // 声優名から対象のスプレッドシートの行を取得
  var foundRows = getRowsByVoiceActor(sheet, voiceActorName);

  // LineにPostする
  if (voiceActorName == 'リスト') {
    var year  = yearMonth.slice(0,4),
        month = yearMonth.slice(4);
    UrlFetchApp.fetch(config.LinePostUrl, createRequest(replyToken, createListPagePostMessage(year, month)));
  } else {
    if (foundRows.length == 0) {
      UrlFetchApp.fetch(config.LinePostUrl, createRequest(replyToken, createNotExistsPostMessage(targetMonth, voiceActorName)));
    } else {
      UrlFetchApp.fetch(config.LinePostUrl, createRequest(replyToken, createPostMessages(sheet, foundRows)));
    }
  }
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

もう少し細かく確認していきましょう。

LINEからPOSTされた情報を受け取る

LINEからは下記のような感じでPOSTデータが返ってきます。
詳しくはMessaging APIリファレンスを読んで見て下さい。

{
  "destination": "xxxxxxxxxx", 
  "events": [
    {
      "replyToken": "0f3779fba3b349968c5d07db31eab56f",
      "type": "message",
      "timestamp": 1462629479859,
      "source": {
        "type": "user",
        "userId": "U4af4980629..."
      },
      "message": {
        "id": "325708",
        "type": "text",
        "text": "Hello, world"
      }
    },
    {
      "replyToken": "8cf9239d56244f4197887e939187e19e",
      "type": "follow",
      "timestamp": 1462629479859,
      "source": {
        "type": "user",
        "userId": "U4af4980629..."
      }
    }
  ]
}

LINEからの入力情報を取得する

  // LineBotからPostされたデータを取得
  var replyToken  = JSON.parse(e.postData.contents).events[0].replyToken,  // WebHookで受信した応答用Token
      userMessage = JSON.parse(e.postData.contents).events[0].message.text; // ユーザーのメッセージを取得(声優名)

replyTokenはイベントへの応答に使用するトークンです。
userMessageはLINEから入力された声優名が入ります。

シートを取得

Googleスプレッドシートには年月の単位でシートができています。
シート名で取得できるようにしています。

名称未設定.png

シートの取得方法

  // 対象の月を取得する
  // 先月、来月などの文字から対象月を取得する
  // 2019年4月16日に実行した場合は下記のように判断する
  // 声優花子   :今月 → 「 0」で返ってくる
  // 先月の声優花子:先月 → 「-1」で返ってくる
  // 来月の声優花子:来月 → 「+1」で返ってくる
  var targetMonth = getTargetMonth(userMessage);

  // 対象のシートを取得
  // 対象の月から年月を取得し、対象のシートを取得する
  // 2019年4月16日に実行した場合は下記のようにシートを判断する
  // 声優花子   :201904
  // 先月の声優花子:201903
  // 来月の声優花子:201905
  // yearMonthには「201904」、「201903」、「201905」などの文字列が取得されます
  var yearMonth = getYearMonth(targetMonth),
      sheet     = getSheet(yearMonth);

年月の情報を取得するメソッド群

// 検索する対象月の値定数
var targetMonth = {
  LastMonth          : -1,
  NextMonth          : 1,
  CurrentMonth       : 0,
  LastMonthString    : '先月の',
  NextMonthString    : '来月の',
  CurrentMonthString : '今月の'
}

// 対象の月情報を取得する
// 文字列に「先月の」、「来月の」などの文字列でどの月が対象か判断する
// 「0」か「1」か「-1」を返す
function getTargetMonth(message) {
  if (message.indexOf(targetMonth.LastMonthString) != -1) {
    return targetMonth.LastMonth
  } else if (message.indexOf(targetMonth.NextMonthString) != -1) {
    return targetMonth.NextMonth;
  } else {
    return targetMonth.CurrentMonth;
  }
}

// 年月の文字列を返す
// addMonthの値はgetTargetMonthで取得した値が来る想定です
// YYYYMMDD形式の文字をを返す
// YYYYMMDD形式がシート名となっているため
function getYearMonth(addMonth) {
  var date = new Date();

  // addMonthの内容により◯ヶ月前、◯ヶ月後の情報をセットする
  date.setMonth(date.getMonth() + addMonth);

  // 月は-1で取得されるため、+1する
  var year  = date.getFullYear(), 
      month = date.getMonth() + 1;

  // 月が1桁時は前0を付加して年月の文字列を返す
  return year + ('0' + month).slice(-2);
}

シートを取得するメソッド

// シート名から対象のシートオブジェクトを取得しています
// SheetNameには「201904」、「201903」、「201905」がくる想定です
function getSheet(SheetName) {
  var ss    = SpreadsheetApp.getActiveSpreadsheet(),
      sheet = ss.getSheetByName(SheetName);
  return sheet;
}

声優名から合致するデータを取得する

声優名から対象の行を抽出する

  // 声優名を取得する
  var voiceActorName = getVoiceActorName(userMessage)

  // 声優名から対象のスプレッドシートの行を取得
  var foundRows = getRowsByVoiceActor(sheet, voiceActorName);

声優名を取得するメソッド

// LINE BOTから受け取った文字列から「先月の」、「来月の」の文字列を排除した
// 文字列を受け取る
// 下記のように感じなります
// 声優花子   :声優花子
// 先月の声優花子:声優花子
// 来月の声優花子:声優花子
function getVoiceActorName(message) {
  replaceMessage = message.replace(targetMonth.LastMonthString, '');
  return replaceMessage.replace(targetMonth.NextMonthString, '');
}

声優名から対象の行を取得するメソッド

// 声優名から対象の行を取得するメソッド
function getRowsByVoiceActor(sheet, voiceActorName) {
  // 声優名列はI列なのでI列を対象に最終行までのRangeオブジェクトを取得します
  var startRow  = 2,
      searchRangeArea = 'I' + startRow + ':I' + sheet.getLastRow(),
      range     = sheet.getRange(searchRangeArea),
      foundRows = [];

  // Rangeのスタートが2行目のため、Rangeの行数+1する
  var searchMaxRow = range.getNumRows() + 1;

  // 声優名列から対象の声優が出演しているか検索し結果を配列にセット
  // 1行ずつ確認していき、対象の声優の文字列がある時は出演していると判断する
  // 声優名列には「声優花子、声優ゴリラ、声優キリン」のような文字列にLINE BOTからの文字列が
  // 含まれているか判断しているだけなので結構ゆるい感じで判断しています 
  for(var row = startRow; row <= searchMaxRow; row++) {
    cellValue = sheet.getRange("I" + row).getValue();
    if (cellValue.indexOf(voiceActorName) != -1) 
      foundRows.push(row);
  }

  // APIの制限で5件までしか送信できないため、最初から5件のみを取り出す
  // リクエストボディのところに5件までと書かれています
  // https://developers.line.biz/ja/reference/messaging-api/#anchor-6640e4a392930e46edb1c15c1d6817ee2356f75e
  return foundRows.slice(0, 5);
}

LINEにPOSTする

UrlFetchAppを使用してLINEにPOSTします。
詳しくはClass UrlFetchApp  |  Apps Script  |  Google Developersを確認して下さい。

リストとそうでない時で処理を切り分けています。
さらに入力された声優名の行が存在する時としない時で切り分けています。

  // config.LinePostUrlにはLINEにPOSTするURLが入っています。
  // 「https://api.line.me/v2/bot/message/reply」になります。

  // LineにPostする
  if (voiceActorName == 'リスト') {
    // 年月の文字列から年と月を切り分ける
    var year  = yearMonth.slice(0,4),
        month = yearMonth.slice(4);
    UrlFetchApp.fetch(config.LinePostUrl, createRequest(replyToken, createListPagePostMessage(year, month)));
  } else {
    if (foundRows.length == 0) {
      UrlFetchApp.fetch(config.LinePostUrl, createRequest(replyToken, createNotExistsPostMessage(targetMonth, voiceActorName)));
    } else {
      UrlFetchApp.fetch(config.LinePostUrl, createRequest(replyToken, createPostMessages(sheet, foundRows)));
    }
  }
// POSTのリクエストを作成している箇所です
// callbackにはLINEのメッセージオブジェクトの配列が入ります
// メッセージオブジェクトについては
// https://developers.line.biz/ja/reference/messaging-api/#message-objects
// を読んで下さい
function createRequest(replyToken, callback) {
  return {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + config.LineAccessToken,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': callback,
    }),
  }
}

メッセージオブジェクトを作成している箇所です
メッセージオブジェクトについてはこちらを読んで下さい

// 対象の声優がゲームに出演している時
function createPostMessages(sheet, rows) {
  // 行数分の配列を作成します
  return rows.map(function(row) {
    return {
      'type': 'text',
      'text': postMessage(sheet, row),
    }
  });
}

 // 対象の声優がゲームに出演していなかった時
function createNotExistsPostMessage(month, voiceActorName) {
  return [{
    'type': 'text',
    'text': notExistPostMessage(month, voiceActorName),
  }]
}

// リストページを返す時
function createListPagePostMessage(year, month) {
  return [{
    'type': 'text',
    'text': listPagePostMessage(year, month),
  }]
}

メッセージ作成メソッド群

// 対象の声優がゲームに出演している時のメッセージの時
function postMessage(sheet, row) {
  // 対象の行データを全て取得する
  var rowValues        = sheet.getRange(row, 1, 1, maxColumnsCount).getValues()[0];

  // メッセージに表示するための情報を取得します
  var releaseDate      = Utilities.formatDate(rowValues[columns.ReleaseDate], "JST", "yyyy/MM/dd"),
      title            = rowValues[columns.Title],
      price            = rowValues[columns.Price],
      introductionPage = rowValues[columns.IntroductionPage],
      brandPage        = rowValues[columns.BrandPage];

  var message = releaseDate + '\n' +
                title + '\n' +
                price + '\n' +
                introductionPage + '\n' +
                '\n' +
                brandPage;

  return message;
}

// 対象の声優がゲームに出演していなかった時のメッセージの時
function notExistPostMessage(month, voiceActorName) {
  var message = '';

  if (month == targetMonth.LastMonth) {
    message = targetMonth.LastMonthString + voiceActorName;
  } else if (month == targetMonth.NextMonth) {
    message = targetMonth.NextMonthString + voiceActorName;
  } else {
    message = targetMonth.CurrentMonthString + voiceActorName;
  }

  message = message + 'はゲームに出演する予定はありません'

  return message;
}


// リストページのメッセージの時
function listPagePostMessage(year, month) {
  var message = year + '年' + month + '月の発売リストページです\n' + 
               'http://www.getchu.com/all/price.html?genre=pc_soft&year=' + year + 
               '&month=' + month + 
               '&gage=&gall=all';
  return message;
}

Slackに通知する

LINE BOTだけのつもりだったのですが、スクレイピングツールで書き込んだ内容が正しいのかどうかを判断するため、Slackにも通知されるようにしました。GASのいいところは定期実行が簡単に指定できるので、決まった時間にSlackに通知させることは容易です。

Slack通知

Slack通知を行うためのコードが下記になります。Slack通知でめんどくさいのがAttachementsの作成です。
逆に言うとAttachementsを頑張って作成するといい感じでSlackに通知されるようになります。Attachementsは通知される時のメッセージのデザインを細かく設定できるものです。

// 発売リスト一覧のメッセージをSlackに送信する
function releaseListSendMessage() {
  // 送信するメッセージ情報を作成する
  var yearMonth = getYearMonth(0),
      sheet     = getSheet(yearMonth),
      foundRows = getAllRows(sheet);
  var attachments = (function(sheet, rows) {
    // 対象のデータを全て取得しそのデータからAttachementを作成する
    var rowsCount = rows[rows.length - 1] - 1,
        values    = sheet.getRange(2, 1, rowsCount, maxColumnsCount).getValues();

    Logger.log(values);

    return values.map(function(value) {
      var brandName        = value[columns.BrandName],
          title            = value[columns.Title],
          introductionPage = value[columns.IntroductionPage],
          releaseDate      = Utilities.formatDate(value[columns.ReleaseDate],"JST","yyyy/MM/dd"),
          price            = value[columns.Price],
          voiceActors      = value[columns.VoiceActor],
          packageImage     = value[columns.PackageImage];
      return createAttachement(brandName, title, introductionPage, releaseDate, price, voiceActors, packageImage);
    });
  }(sheet, foundRows));

  // メッセージを作成する
  var year    = yearMonth.slice(0,4),
      month   = yearMonth.slice(4),
      message = year + '年' + month + '月の発売リスト';

  // Slackにメッセージを送信する 
  sendMessage(message, 
              config.SlackPostChannel, 
              '発売リストくん', 
              config.SlackPostUserIcon, 
              attachments);
}

// Attachementを作成する
function createAttachement(brandName, title, introductionPage, releaseDate, price, voiceActors, image) {
  return {
      "author_name": brandName,
      "color": "#a8bdff",
      "title": title,
      "title_link": introductionPage,
      "text": releaseDate + '\n' + 
              price + '\n' + 
              voiceActors,
      "image_url": image
  }
}

// Slackのあるチャンネルにメッセージを送信する
function sendMessage(message, channel, username, iconUrl, attachments) {
  var payload = {
    channel: channel,
    text: message,
    username: username,
    icon_url: iconUrl,
    attachments: attachments,
  };

  var option = {
    'method': 'post',
    'payload': JSON.stringify(payload),
    'contentType': 'application/x-www-form-urlencoded; charset=utf-8',
    'muteHttpExceptions': true
  };

  // Slackにメッセージを送信
  var response = UrlFetchApp.fetch(config.SlackWebHookUrl, option);
}

設定方法としては、スクリプトエディタからトリガーを設定するだけです。

sample_trigger_button.png

トリガー設定画面
設定は関数単位で指定できるのでreleaseListSendMessageを設定します。

torigger_scrreen

sample_trigger_page.png

この設定で1週間ごと、毎週金曜日にSlackに通知されるようになります。

まとめ

Googleスプレッドシートへの書き込みがまだ手動なので今後はここを自動化したいと思っています。

開発していて一番時間がかかったのはRubyのRSpecです。1ヶ月以上戦っていた気がします。LINE BOTとGASに関しては触ったことがなかったのですが10時間ぐらいでできました。
10時間ぐらいなので休みの日をかけてLINE BOTを余裕で作成できるレベルです。

皆さんも気軽にLINE BOTを作成してみるのものいいかもしれません!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】コーディング初学者が「for – of文 」と「for文」と 関数の理解を深める

はじめに

テスト自動化ができる様になりたくて、「Javascript」を勉強していた時に「for – of文 」と「for」文ってどっちも配列の中身を取得できるけど、結局なにが違うの?と迷子になったので、違いの備忘録。

備忘録を書いている間に勉強になる事がいろいろあったので私の思考回路をメモしておきます。

多分初心者は同じ道たどるのかな・・・?

for文のおさらい

自分で記事を書いているうちに頭パーンしたので、for-of と forの違いをおさらい!!

・for-of文は配列から1要素ずつ取り出して、繰り返しできる構文
 1要素取り出す ⇒ {}の中身を実行 ⇒ 次の要素を取り出す ・・・・・

・for文は繰り返しの為の変数を用意して継続条件を満たす間繰り返してくれる構文
 インデックス番号の変数を定義 ⇒ {}の中に繰り返す変数を代入して実行 ⇒ 変数を変える ⇒ {}の中・・・

これだけみると、繰り返しの為に1要素ずつ取り出すという行為を 「必ず1つずつ取り出して」から、内容を実行するか「インデックス番号を指定して」から内容を実行するかの違いはありそう!!

例えるならば、後ろから順に取得したりとか、キリのいい数字で飛ばし飛ばし取得するのは「for文」しかできなさそうで、最初から順番に取得する場合は違いはあまりなさそうですね!!

⇒ ならば試しにやってみよう

配列を用意する

数字だとイメージが湧きにくかったので、ある幸せそうな家族の「名前、年齢、身長、体重」のデータを用意しました

index name age height weight
0 たろう 43 178 83
1 はなこ 40 158 60
2 なな 8 124 25
const humans = [{
  name:"たろう",
  age:43,
  height:178,
  weight:83
},{
  name:"はなこ",
  age:40,
  height:158,
  weight:60
},{
  name:"なな",
  age:8,
  height:124,
  weight:23
}];

コンソールにループさせた内容を出力してみる

「for – of文 」で配列をループさせる

for (let human of humans){   
  console.log(human);     
}


//以下出力内容
{name: "たろう", age: 43, height: 178, weight: 83}
{name: "はなこ", age: 40, height: 158, weight: 60}
{name: "なな", age: 8, height: 124, weight: 23}

中のオブジェクトが順番に出力される
配列の要素を1つずつ取り出して、Console.log実行

「for」文で配列ループさせる

for (i = 0 ; i < humans.length ; i++){
  console.log(humans[i]);
}

//以下出力内容

{name: "たろう", age: 43, height: 178, weight: 83}
{name: "はなこ", age: 40, height: 158, weight: 60}
{name: "なな", age: 8, height: 124, weight: 23}

これも中のオブジェクトが順番に出力される。
humans[0]を実行 ⇒ 「配列の1つ目の要素を出力してー」 ×3

出力に違いってあるの?

「出力される内容」の違いはなかった。しかし、for内で定義している変数の対象が違うという発見!(あたりまえ)

☆「for-of文」は「出力した内容」が変数として定義される(⇒logのプロパティには、forの条件文の中で定義した変数を入れている)

☆「for 文」は変数は繰り返しの為の数値でインデックス番号として使用されている(⇒ iはループさせる為の数値、これがインデックス番号になるので、logのプロパティには、”配列の変数名"[i]となっている

イメージをふくらませると、「順番は最初から決まってるから、この順番(for-of文)通りに終わるまでよろしくねー」と「1番目!!やり方はこれだ!! 2番め!!やり方はさっきと一緒!! 3番目!!」みたいに順番を明確に指定してあげてるみたいかんじかな?

つまり、配列の中身を1番めからすべて出力する場合どちらを使っても問題ない。。。ハズ。。。。
(※実行スピードや、対応ブラウザ等は全く考慮してません)

指定のプロパティのデータを取得してみる

出力された値は変わらなさそうなので、次は「for – of文 」と「for文」を使って、配列の中に入っているオブジェクトのプロパティのデータ(今回は名前)を取得して違いを比較してみます

おさらい
スクリーンショット 2019-04-16 21.39.34.png

「for – of文」で配列の中のプロパティのデータを取得する

for (let human of humans){
  console.log(human.name);  //console.log(human["name"])でも可
}

//出力内容は以下

たろう
はなこ
なな

変数humansから1つ目の要素をとりだしたのをhumanに入れている間に{プロパティ名"name"のデータを出力する}を実行してね、それがhumansの要素の最後までおねがいね!

※それてしまうけれど、「for文の外で 変数"human"つかったらどうなるの?」と思って試した事は以下

//1. for終了後に、変数humanを呼び足してみたらどうなるか

for (let human of humans){
  console.log(human.name);
}
console.log(human);

//出力内容

Uncaught ReferenceError: human is not defined
//humanなんて変数定義されてないよ

--------------------------------------------------------------------

//2 human.nameの出力の後にfor文内で、変数humanを出力したらどうなるの?

for (let human of humans){
  console.log(human.name);
  console.log(human);
}

//出力内容

たろう
{name: "たろう", age: 43, height: 178, weight: 83}
はなこ
{name: "はなこ", age: 40, height: 158, weight: 60}
 なな
{name: "なな", age: 8, height: 124, weight: 23}

↓私の思考の遍歴です。
なるほど。代入演算子"="が使われていないから、for-of文内ではないと、for文内で定義した変数名はつかえないってことね!!ちょっと頭がすっきりしたきがする 

この部分は「代入演算子」の問題ではなく、変数のスコープと言われるものらしいです。
ブロック内{ }で宣言されたletは、ブロック外では使えないという決まりです。
複雑になればなるほどわからなくなっていくので、できる限りグローバル変数は使わない方がいいらしいです!

(そしてこんなふうに考えてた自分もいるっていうのを残す為、あえて消さないでおきます(´・ω・`))

「for文」で配列の中のプロパティのデータを取得する

for (i = 0 ; i < humans.length ; i++){
  console.log(humans[i].name);  //console.log(humans[i]["name"])でもok
}

//出力内容は以下
たろう
はなこ
なな

変数humansの[i]番目の要素のnameプロパティにはいっているデータを出力してね。

そして今回は代入演算子"="がつかわれているから、forの外で"変数i"出力したら「3」が返ってくるはず!!

console.log(i);

//出力内容


キタ━━━━(゚∀゚)━━━━!!予想的中!!
代入演算子がないと、文の外ではその変数つかえなくなるってことね!!
すごーくすっきりした!!

だからお前変数のスコーp(ry
ちなみに i = 0 は、 var i = 0 のことで、varはグローバルスコープになるから、「3」の結果が出力されたって事みたい!

この件再度フィードバックもらえました!!
JSだと、 i = 0の様に letvar を省略すると、「グローバル変数」になってしまうそうです.
実践では、{ }の内部にグローバル変数を持たせない方がいいとの事なので
ES6をつかっているのであればfor ( let i = O ; ~ letを省略をせずに書いた方がよさそうです。

forで出力した内容で関数をつかってみる

それならついでに、forで出力した内容を使って関数でかいてみる!
今回は、身長と体重からBMIを出力する関数(に使用する引き数を)「for-of」と「for」を使って比較をしてみる

const bmicast = (height, weight) => {        //引数(height , weight)を受け取る関数作成
  const cast = weight / (height ** 2);      //BMI = 体重 / (身長 * 身長)
  const bmi = Math.floor(cast);             //小数点以下をきりすて
  console.log(bmi);                        //BMI出力してね
  }

for(let human of humans){
  let height = human.height / 100;  //身長をメートルに換算
  let weight = human.weight;       
  bmicast(height, weight); //関数呼び出し
}

//出力内容

26
24
14

forで出力した内容を使ってみる

const bmicast = (height, weight)  => {
  const cast = weight /( height ** 2 );
  const bmi = Math.floor(cast);
  console.log(bmi);
}

for(i = 0 ; i < humans.length ; i++){
  let height = humans[i].height / 100; //メートルに換算
  let weight = humans[i].weight;     
  bmicast(height, weight); //関数呼び出し
}


//出力内容
26
24
14

うんこれも出力内容かわらないね。。

⇒それなら、出力したBMIを配列に追加したらどうなるだろう!!

出力したBMIを配列に追加をする

「for文」をつかってオブジェクトにプロパティを追加する

少し変数名をさわっていますが、繰り返し処理の最後に、配列操作しています

const addBMI = (height, weight)  => {       // addBMIという関数を定義
  const cast = weight /( height ** 2 );     // BMIの計算
  const bmi = Math.floor(cast);             // 少数点以下を切り捨て
  humans[i].bmi = bmi;                      //  配列humansの i番目の一番うしろにプロパティ名bmiとして、変数bmiを代入
}

for(i = 0 ; i < humans.length ; i++){
  let height = humans[i].height / 100; //メートルに換算
  let weight = humans[i].weight;
  addBMI(height, weight);
}


console.log(humans); // 変数humasを出力

//以下出力内容

[{
 name: "たろう", age: 43, height: 178, weight: 83, bmi: 26
},{
 name: "はなこ", age: 40, height: 158, weight: 60, bmi: 24
},{
 name: "なな", age: 8, height: 124, weight: 23, bmi: 14
}]


オブジェクトの最後に計算されたBMIが追加されました!!
[i]番目の配列に、プロパティ名bmiのプロパティを追加するという処理だけなので簡単です!!
⇒ この書き方は関数のスコープがしっかりしてないから良くないらしい。。。関数の変数内は関数内でおさめた方が今後の為だそうです!

「for文」 レビュー後

const addBmi = (height, weight, human) => {
  const cast = weight / (height ** 2); //bmi計算
  const bmi = Math.floor(cast)         //切り捨て
  human.bmi = bmi  //オブジェクトの最後にbmiを追加
}

for (i=0 ; i < humans.length ; i++){
  const height = humans[i].height / 100 ;  //メートルに換算
  addBmi(height, humans[i].weight, humans[i]);  //関数呼び出し
}

console.log(humans);


まずはじめに
let weight = humans[i].weight; はいらないよー!
引数にあてはめる時にそのまま、weightプロパティを取得できるという事でした!!

また引数にhumans[i] (配列のhumansのi番目のオブジェクト)をわたして
関数内で、humans[i].bmi = bmiと同じ状況をつくってあげています!
こうすることで、グローバル変数をつかわなくて済むとの事です。

「for - of文」をつかってオブジェクトにプロパティを追加する

これで書き換えようとおもった時に、全然いい方法がおもいつかなかったので、社内エンジニアの方にたすけてもらいました。

それがこちら

const reBmi = (height, weight) => { //bmiをリターンする関数を定義
  const cast = weight / (height ** 2); //bmi計算
  const bmi = Math.floor(cast); //きりすて
  return bmi;
}

for(let human of humans){
  let height = humam.height / 100;    //メートル換算
  human.bmi = reBmi(height, weight);  // bmiをプロパティ名、reBmi関数のリターンをデータとして
}                                    // 取り出したオブジェクトに追加する

console.log(humans);

なるほどこういう風に書けばいいのか。。!!

最後に

最初は「for – of文 」と「for文」のちがいがよくわからずに、違いがあるのかな?と思って始めたものが、気づいたら関数の取扱いの方で迷子になってました(´・ω・`)
「for – of文 」と「for文」はお好みでどーぞだと思います!

関数は値として扱える
関数は何をいれて、何を出すのかを意識して考える

関数は書き方を覚えるしかない気がしてきました。
そして、配列操作はもう一回勉強しなおさないと。。。!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React-Day-Pickerのスタイルを変える方法

日付選択機能が拡張しやすそうというで理由でblueprintというUI Toolkitを使っているのだが、実際に使ってみて見た目を変えたいなと思った所、結構癖があったのでメモしていく。

blueprintのDatePickerはReact-Day-Pickerのラッパー的なもの

blueprintのDatePickerはReact-Day-Pickerをベースにしていて、プロパティ経由で簡単にReact-Day-Pickerのプロパティを指定することができる。

ので、見た目を変えるときはReact-Day-Pickerの機能を使う必要が出てくる。

React-Day-Picker Styling

export function Test() {
  const customProps = { ... } //React-Day-Pickerの設定
  return <DatePicker dayPickerProps={customProps} />
}

スタイルを変えるときに取ることが出来る方法は以下のものがある。

ここで注意するのが、日にちに対するスタイルの指定とUI全体のスタイルの指定の二種類ある
(classNameも用意されている)

カレンダーの日にちのスタイルを変更する

  • modifiers
  • modifiersStyles

DayPicker全体のスタイルを変更する

  • classNames

日にちのスタイルを変更する

React-Day-Pickerではある日にちをいじりたいときはmodifiersプロパティを使って指定する形をとっている。

その際グループ化したい日にちのパターンとその名前という形で設定していく。

設定したグループの日にちにはDayPicker-Day--<グループ名>という名前のCSSクラスの名前が与えられるので
それに対してスタイルを設定すると好きな見た目に変更することが出来る。

もちろん自動生成されたCSSクラス名をCSSファイルに書かないといけない訳ではなく、modifiersStylesにグループ名をキーをとしたinline-Styleを設定すればいい。

Matching days with modifiersにあるサンプルコードをちょっと変えたもの。

import React from 'react';
import DayPicker from 'react-day-picker';
import 'react-day-picker/lib/style.css';

const birthdayStyle = `.DayPicker-Day--highlighted {
  background-color: orange;
  color: white;
}`;

const modifiers = {
  highlighted: new Date(2018, 8, 19),
};
const modifiersStyles = {
  highlighted: {
    color: '#FF0000'
  }
}
export default function MyBirthday() {
  return (
    <div>
      <style>{birthdayStyle}</style>
      <DayPicker
        modifiers={modifiers}
        modifiersStyles={modifiersStyles}
        month={new Date(2018, 8)} />
    </div>
  );
}

日にちの指定の仕方

グループを作る際の日にちの指定には以下の種類があるので適当に使い分ける。

グループは複数指定でき、ある日にちが二つ以上のグループにあるときはその全てが適応される。

  • from/toキー この二つに与えたDateオブジェクトの範囲内の日にちをグループ化する
  • beforeキー 与えた日にちより前のものをグループ化する
  • afterキー 与えた日にちより後のものをグループ化する
  • daysOfWeek 指定した曜日でグループ化する。曜日は0から6の範囲を取り、それぞれ日曜から土曜に対応している。配列で渡すと複数の曜日を指定できる。
  • function(day: Date):boolean グループ化する日にちのときはtrueを返すDateオブジェクトを受け取る関数
const from_to = [
  from: new Date(2018, 8, 19),
  to: new Date(2018, 9, 19),
]
const before_after = {
  after: new Date(2018, 9, 1),
  before: new Date(2018, 10, 1)
}
const daysOfWeek = {
  daysOfWeek: [0] // 日曜日
}
const func = (day) => { return day.getDate() % 2 === 0}

export default function Grouping() {
  return (
    <div>
      <style>{birthdayStyle}</style>
      <DayPicker
        modifiers={[from_to, before_after, daysOfWeek]} />
    </div>
  );
}

用意されているグループ

ちなみにtodayoutsideが予め用意されており、それぞれ今日の日にちと現在の月以外の日にちが対象になる。

イベントとの組み合わせ

グループはDayPickerのイベントが起きたときに日にちを識別する目的にも使うことが出来る。

グループ情報を参照することが出来るイベントにはmodifiersがオブジェクトとして引数に渡されていて、
そのオブジェクトには所属しているグループの名前をboolean型として持っている。

//ドキュメントから拝借
import 'react-day-picker/lib/style.css';

export default class EventHandlers extends React.Component {
  constructor(props) {
    super(props);
    this.handleDayClick = this.handleDayClick.bind(this);
    this.handleDayMouseEnter = this.handleDayMouseEnter.bind(this);
  }

  handleDayMouseEnter(day, { firstOfMonth }) {
    if (firstOfMonth) {
      // Do something when the first day of month has been mouse-entered
    }
  }

  handleDayClick(day, { sunday, disabled }) {
    if (sunday) {
      window.alert('Sunday has been clicked');
    }
    if (disabled) {
      window.alert('This day is disabled');
    }
  }

  render() {
    return (
      <DayPicker
        disabledDays={new Date()}
        modifiers={{
          sunday: day => day.getDay() === 0,
          firstOfMonth: day => day.getDate() === 1,
        }}
        onDayClick={this.handleDayClick}
        onDayMouseEnter={this.handleDayMouseEnter}
      />
    );
  }
}

と、かなり汎用性があるものになっている。

DayPicker全体のスタイルを変更するときはclassNames用のテンプレートを使おう

日にちについてはかなり柔軟なカスタマイズが出来るようになっているが、
それ以外の例えばヘッダー部分とかの背景色を変更したいときはclassNamesプロパティを使う。

classNamesにはDayPickerで使われる要素全てにCSSを指定する形になっている。

各要素には使用するCSSクラスを指定するのだが、要素の数は結構あるので公式から用意されているテンプレートを書き換えるのがいいと思う。

テンプレートはこちら react-day-picker/src/classNames.js

テンプレートのインターフェイスはimport {ClassNames} from 'react-day-picker'からインポート出来る。

function makeClassNames(config) {
  const DEFAULT = {
    container: 'DayPicker',
    wrapper: 'DayPicker-wrapper',
    interactionDisabled: 'DayPicker--interactionDisabled',
    months: 'DayPicker-Months',
    month: 'DayPicker-Month',

    navBar: 'DayPicker-NavBar',
    navButtonPrev: 'DayPicker-NavButton DayPicker-NavButton--prev',
    navButtonNext: 'DayPicker-NavButton DayPicker-NavButton--next',
    navButtonInteractionDisabled: 'DayPicker-NavButton--interactionDisabled',

    caption: 'DayPicker-Caption',
    weekdays: 'DayPicker-Weekdays',
    weekdaysRow: 'DayPicker-WeekdaysRow',
    weekday: 'DayPicker-Weekday',
    body: 'DayPicker-Body',
    week: 'DayPicker-Week',
    weekNumber: 'DayPicker-WeekNumber',
    day: 'DayPicker-Day',
    footer: 'DayPicker-Footer',
    todayButton: 'DayPicker-TodayButton',

    // default modifiers
    today: 'today',
    selected: 'selected',
    disabled: 'disabled',
    outside: 'outside',
  };
  return Object.assign({}, DEFAULT, config)
}

export default function CustomClassNames() {
  //
  const classNames = makeClassNames({
    container: 'changeBackground'
  })
  return (
    <DayPicker 
      classNames={ styles } 
      modifiers={{
        [styles.birthday]: new Date(2018, 8, 19)
      }}
    />
  );
}

ちなみにデフォルトで設定されているクラスを完全に上書きしてしまうとレイアウトが崩れたので、次のように追記する形にしたほうがいいと思う。

指定したクラス名はそのまま設定されるようなので、classnamesパッケージとか利用したら便利。

const custom = {
  caption: classnames('DayPicker-Caption', 'appendStyles')
}

もちろんCSSModuleも使える

ので、好きな方法でスタイルをカスタマイズすることが出来る。

かなり拡張性が高いパッケージになっていて、日時選択にはこれが一番融通がきくのではないかなと。

おすすめのパッケージです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsをvscodeでデバッグするためのメモ

前提

  • Nuxt.jsの動作モードは universal(SSR有効)

  • vscodeのextensionとしてDebugger for Chrome がインストールされている

やりたいこと

  1. vscodeからNuxt.jsのデバッグをしたい
  2. かつ、vscodeの行クリックで設定したブレークポイントで、エディタのデバッグモード上のbreakをかけたい(debuggerを明に埋め込むのではなく)
  3. 実際にbreak中に表示されるコードは、本来編集しているコードとしたい

上記2はいまいち伝えにくいのですが、こういうふうにbreakしたいということです。

1.png

3は、build後のコードではなく、本来実装社が見ているコード上でデバッグが行えるようにしたいという意味です。これを無視すれば、 debugger (という記載)を仕込むだけで大半片付くわけですが。

で、以下のようになりました。

nuxt.config.jsを修正

nuxt.config.jsに以下の行を足します。

SourceMapとはなんぞやという人(自分はそうでした)は こちら をご参照ください。

  build: {
    ()
    extend(config, ctx) {
      ()
      // Run ESLint on save
      if (ctx.isDev && ctx.isClient) {
        config.devtool = 'inline-cheap-module-source-map' // <-- ここを足す
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/,
        })
      }
    },
  },

以降は多分に こちら を参考にしました。

package.jsonを修正

{
  "name": "nuxt-bulma",
  (略)
  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
    "build": "nuxt build",
    "start": "cross-env NODE_ENV=production node server/index.js",
    "generate": "nuxt generate",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
    "precommit": "npm run lint",
    "test": "ava",
    "debug": "node --inspect-brk=9229 node_modules/nuxt/bin/nuxt" <-- 追記
  },

このキーとなっている "debug"は、次のlauhch.jsonのruntimeArgs と合わせる必要があります。

launch.jsonを修正

こうなりました。

{
  // IntelliSense を使用して利用可能な属性を学べます。
  // 既存の属性の説明をホバーして表示します。
  // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch via NPM",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run-script", "debug"],
      "port": 9229,
      "sourceMaps": true
    },
    {
      "type": "chrome",
      "request": "launch",
      "name": "Launch Chrome",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}"
    }
  ],
  "compounds": [
    {
      "name": "Full-stack",
      "configurations": ["Launch via NPM", "Launch Chrome"]
    }
  ]
}

この状態で、デバッグ用の再生ボタンを押すとブラウザが新規に開きます。

スクリーンショット 2019-04-17 20.41.26.png

その後一定時間が経過するとブラウザウィンドウでbuildのプログレスが表示されるようになり、その後ブレイクがかかる部分の処理を実行すると、vscode上でブレイクがかかります。

いやー、調べるのにいろいろ時間がかかりました。

ひとえに自分のNuxt.jsおよびJavaScriptの知識不足ですね。

情報を公開してくださっていた皆様に感謝します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】 jQueryが動作しない時の対処法 (読み込む順番)

いつも忘れてしまうので備忘録。

ダメなパターン

_head.html.erb
<%= stylesheet_link_tag    'style', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'selectordie' %>

<!-- JQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

javascript_include_tagjQuery CDN の順番では期待通りになりませんでした。

正のパターン

_head.html.erb
<!-- JQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

<%= stylesheet_link_tag    'style', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'selectordie' %>

jQuery CDNjavascript_include_tag の順番だと期待値通りの動作!

まとめ

先にjQueryロードしないとスクリプト読めないって話。(Railsに限った話じゃない・・・)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】 jQueryが動作しない時の対処法 (読み込む順番に気をつけて)

いつも忘れてしまうので備忘録。

ダメなパターン

_head.html.erb
<%= stylesheet_link_tag    'style', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'selectordie' %>

<!-- JQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

javascript_include_tagjQuery CDN の順番では期待通りになりませんでした。

正のパターン

_head.html.erb
<!-- JQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

<%= stylesheet_link_tag    'style', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'selectordie' %>

jQuery CDNjavascript_include_tag の順番だと期待値通りの動作!

まとめ

先にjQueryロードしないとスクリプト読めないって話。(Railsに限った話じゃない・・・)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】 jQuery/jsファイル 読み込む順番

いつも忘れてしまうので備忘録。

ダメなパターン

_head.html.erb
<%= stylesheet_link_tag    'style', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'selectordie' %>

<!-- JQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

javascript_include_tagjQuery CDN の順番では期待通りになりませんでした。

正のパターン

_head.html.erb
<!-- JQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

<%= stylesheet_link_tag    'style', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'selectordie' %>

jQuery CDNjavascript_include_tag の順番だと期待値通りの動作!

まとめ

先にjQueryロードしないとスクリプト読めないって話。(Railsに限った話じゃない・・・)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebページのタイトルとURLをコピペするブックマークレット

これは何?

押したらタイトルとURLコピペできるブックマークの作り方を紹介する記事です。
Image from Gyazo
Chromeメニュー>表示>ブックマークバーを常に表示 をオンにしています。

ブックマークレット超初心者向けの手順で書いてます。

作り方

①これをコピー

javascript:var global=window;global.COPY_TO_CLIPBOARD=global.COPY_TO_CLIPBOARD||{};global.COPY_TO_CLIPBOARD.getUrlInfo=function(){var a=new String(document.title);a.allReplace=function(a){var b=this,c;for(c in a)b=b.replace(new RegExp(c,"g"),a[c]);return b}.bind(a);return""+a.allReplace({":":"\uff1a","\\[":"\uff3b","\\]":"\uff3d"})+" "+document.URL};global.COPY_TO_CLIPBOARD.copyToClipboard=function(){var a=document.createElement("textarea");a.textContent=this.getUrlInfo();var d=document.getElementsByTagName("body")[0];d.appendChild(a);a.select();var b=document.execCommand("copy");d.removeChild(a);return b};global.COPY_TO_CLIPBOARD.copyToClipboard();

②何でもいいからブックマークを追加

例えば星を押す
スクリーンショット 2019-04-17 19.23.08.png

③ブックマーク編集画面を呼び出す

↑の画面で「その他」を押せば出てきます
スクリーンショット 2019-04-17 19.23.50.png

④タイトルとURLをいじる

タイトルには好きなタイトルを入力、URLには①でコピーしたコードをペースト
スクリーンショット 2019-04-17 19.25.28.png

⑤保存して完成

お疲れ様でした。

⑥好きなWebページで追加したブックマークを呼び出す

こんな感じ(タイトル - URL)にコピーされます
好きなところにペーストしてください

モホロビチッチ不連続面 - Wikipedia https://ja.wikipedia.org/wiki/%E3%83%A2%E3%83%9B%E3%83%AD%E3%83%93%E3%83%81%E3%83%83%E3%83%81%E4%B8%8D%E9%80%A3%E7%B6%9A%E9%9D%A2

Ref

コードはほとんど以下の@xshojiさんのものを使わせていただいております。
WebページのタイトルとURLをワンクリックでコピーするBookmarklet - Qiita https://qiita.com/xshoji/items/93d5345d4bf282f60817

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebpackでUglifyJSPluginを使ってminifyしようとするとUnexpected token: name (Dom7)が出る場合の対処法

TL;DR

  • Swiperを普通に読み込むとDom7が原因でエラーが出る。
  • コンパイルされたes5のファイルを読み込むか、WebpackでDom7をexcludeすることで回避できる。

関連パッケージのバージョン

  • @babel/core: 7.1.2
  • @babel/polyfill: 7.0.0
  • @babel/preset-env: 7.1.0
  • babel-eslint: 10.0.1
  • babel-loader: 8.0.4
  • mini-css-extract-plugin: 0.4.5
  • swiper: 4.5.0
  • uglifyjs-webpack-plugin: 2.0.1
  • webpack: 4.25.1
  • webpack-cli: 3.1.

Webpack

webpack.config.js
const path = require("path"),
  webpack = require("webpack"),
  UglifyJSPlugin = require("uglifyjs-webpack-plugin")
... 
module.exports = (env, argv) => {
  const IS_DEVELOPMENT = argv.mode === "development"

  return {
    entry: ["@babel/polyfill", "./src/js/main.js"],
    output: {
      path: path.join(__dirname, "./dist/"),
      filename: "js/bundle.js"
    },
    devtool: IS_DEVELOPMENT ? "source-map" : "none",
    optimization: {
      minimizer: IS_DEVELOPMENT
        ? []
        : [
            new UglifyJSPlugin({
              uglifyOptions: {
                compress: {
                  drop_console: true
                }
              }
            })
          ]
    },
    ...
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: [
            {
              loader: "babel-loader",
              options: {
                presets: [["@babel/preset-env", { modules: false }]]
              }
            }
          ]
        }
      ]
    }
    ...
  }
}

エラー内容

ERROR in js/bundle.js from UglifyJs
Unexpected token: name (Dom7) [js/bundle.js:10622,6]

解決策

UMD版のみ読み込む

SwiperはES6を使用しているため、コンパイルされたES5のUMD版を使用すれば問題ないようで、読み込み方を変えれば問題なくビルドされる。
ただしこの方法だと当然Tree Shakingが使えない。

import Swiper from "swiper/dist/js/swiper.js"

WebpackでDom7をexcludeする

webpack.config.js
module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /node_modules\/(?!(dom7|swiper)\/).*/,
      use: [
        {
          loader: "babel-loader",
          options: {
            presets: [["@babel/preset-env", { modules: false }]]
          }
        }
      ]
    }
  ]
}
import Swiper from "swiper"

Tree Shakingを使いたい場合

swiper/dist/js/swiper.esm.jsをから使いものだけimportすれば良い。

Swiper API#Custom Build

import { Swiper, Navigation, Pagination, Scrollbar } from 'swiper/dist/js/swiper.esm.js'

おまけ

  • Tree Shaking適用前: 495 KB
  • Tree Shaking適用後: 433 KB

参考

Webpack Production Bundling fails because of UglifyJS Error with Dom7 #2263

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CoreUIを既存のNuxtプロジェクトに導入したメモ

前提条件

npx create-nuxt-app
で既に作成されたnuxtプロジェクトがありました。
一部、ディレクトリ構成などがカスタマイズされており、
新規プロジェクトとして作成することが難しかった。

STEP0 準備 テンポラリのプロジェクトを作成

https://github.com/muhibbudins/nuxt-coreui
ここからソースを取得する。

vue-cliが必要なのでインストールしておく。

yarn global add @vue/cli

nuxt-coreuiインストール

vue init muhibbudins/nuxt-coreui my-project 
yarn install
yarn dev

localhost:3000にアクセスし確認する。

STEP1 必要なモジュールを取得する。

既存のpackage.jsonとインストールしたnuxt-coreuiのpackage.jsonを比較する。

  "dependencies": {
    "@nuxtjs/style-resources": "^0.1.1",
    "chart.js": "^2.7.1",
    "flag-icon-css": "^2.9.0",
    "font-awesome": "^4.7.0",
    "simple-line-icons": "^2.4.1",
    "vue-chartjs": "^3.1.1"
  },

この辺りが不足していた
node-sass, sass-loader, boorstrap-vueは既存プロジェクトに入っていた
chart系は今回利用しないので、それ以外をpackage.jsonに追記

yarn install

STEP2 コンポーネント、SCSSのコピー

インストールしたnuxtーcoreuiプロジェクトのcomponentsのcharts以外のファイルとディレクトリを
既存のプロジェクトのcomponentsの中にコピーする。

インストールしたnuxtーcoreuiプロジェクトのassetsのファイルとディレクトリを
既存のプロジェクトのassetsの中にコピーする。

同様に進めるが、layouts, pages, staticはdefault.vueなど既存のファイルと名前が衝突しがちなので気をつける。
衝突したらdefault-coreui.vueなどと変更して保存しておく。

STEP3 設定の変更

cssのビルドが通るように nuxt.config.jsを変更する。

  css: [
    /* Import Font Awesome Icons Set */
    '~/node_modules/flag-icon-css/css/flag-icon.min.css',
    /* Import Font Awesome Icons Set */
    '~/node_modules/font-awesome/css/font-awesome.min.css',
    /* Import Simple Line Icons Set */
    '~/node_modules/simple-line-icons/css/simple-line-icons.css',
    /* Import Bootstrap Vue Styles */
    '~/node_modules/bootstrap-vue/dist/bootstrap-vue.css',
    /* Import Core SCSS */
    { src: '~/assets/scss/style.scss', lang: 'scss' }
  ],

これで yarn devし、localhost:3000/register などを見てみる。
うまく表示されていたら、既存のページにcore-uiのコンポーネントをimportして利用することが可能。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

高速フーリエ変換FFTを理解する

この文書は、離散フーリエ変換DFTの理解をもとに、高速フーリエ変換FFTの振る舞いを理解することを目標とするものです。

はじめにDFTが何をするものかを簡単に説明し、そのDFT処理をJavaScriptコードに書き下し、FFT実装の検証対象となる具体例でのDFT実行結果を示します。
そして、再帰版FFT実装を天下り的に提示し、具体例を用いてFFTの中で何が行われるかを解析します。
最後に、この解析結果にもとづいて、ループ版FFTを導出します。

1. 離散フーリエ級数(DFT)

離散フーリエ変換(Discrete Fourier Transform, DFT) は、一定期間の時系列の数値列を、その期間を周期的に繰り返す合成波とみなし、その周波数成分の複素数値の列(低周波数から高周波数の順)を算出する仕組みです。

各周波数における複素数値のうち、実数成分が偶関数であるcos波の係数であり、虚数成分が奇関数であるsin波の係数となります。
この係数の量が、時系列データの分析のために用いることができます。たとえば、一番大きい係数の周波数を取り出して音階を算出する、などが行えます。

離散フーリエ変換では、時系列データがN個な場合、区間Nで1/2周期とする周波数を基本に、0倍(定数成分)からN-1倍までのN個の周波数成分を算出します。
時系列データを$f(t), t = 0 \dots N-1$、周波数成分を$F(k), k=0 \dots N-1$とすると、この関係は数式で以下のようになります。

\begin{align}
F(k) = & \sum^{N-1}_{n=0} f(n) \exp(\frac{-2 \pi i}{N} n k) \\
f(t) = & \frac{1}{N} \sum^{N-1}_{n=0} F(n) \exp(\frac{2 \pi i}{N} n t) \\
\end{align}

$f$も$F$もN要素の複素数値配列として、前者の右辺の計算で$F(k)$計算することを「離散フーリエ変換(IFT)」、
後者の右辺の計算で$f(t)$を計算することを、「逆離散フーリエ変換(Inverse DFT,IDFT)」と呼びます。

右辺にある$\exp(i\theta)$の部分は複素数であり、オイラーの公式$\exp(i\theta) = cos{\theta} + i sin{\theta}$となります。
離散フーリエ変換のプログラミングでは、計算でsinやcosを使い、実数成分と虚数成分のペアとして、各成分ごとにコーディングすることも多いです。
この場合、複素数の足し算と掛け算を、以下のように、各成分ごとに行います。

\begin{align}
a + b = & (a_x + i a_y) + (b_x + i b_y) \\
      = & (a_x + b_x) + i(a_y + b_y) \\
a \times b = & (a_x + i a_y) \times (b_x + i b_y) \\
           = & (a_x \times b_x - a_y \times b_y) + i(a_x \times b_y + a_y \times b_x) \\
\end{align}

この実数虚数成分を分けての計算の場合には、さらに「時系列データは実数値のみ」として、以下のように虚数部の計算を省くこともあります(ただし、ここではこの実数限定な実装は行いません)。

  • DFT: $f(t)$の虚数部が0であることから、$exp(i)$の複素数との積は、実数部虚数部ともに$f(t)$をかけるだけにする
  • IDFT: $f(t)$の実部側($a_x \times b_x - a_y \times b_y$)のみ計算する

2. JavaScriptでのDFT実装コード

まず、数値2要素Arrayを複素数として、複素数計算のヘルパーを用意します。
$\exp(i\theta)$は、実数thetaを引数に取り、複素数を返す関数expi(theta)として用意します。
$\sum$に対応するものとして、複素数配列csの総和を取る関数isum(cs)も用意します。

[1]
function expi(theta) {return [Math.cos(theta), Math.sin(theta)];}
function iadd([ax, ay], [bx, by]) {return [ax + bx, ay + by];}
function isub([ax, ay], [bx, by]) {return [ax - bx, ay - by];}
function imul([ax, ay], [bx, by]) {return [ax * bx - ay * by, ax * by + ay * bx];}
function isum(cs) {return cs.reduce((s, c) => iadd(s, c), [0, 0]);}

複素数の時系列データfを受け取り、複素数の周波数成分配列を返すDFT関数dft(f)は、以下のようになります:

[2]
function dft(f) {
    const N = f.length, T = -2 * Math.PI / N;
    return [...Array(N).keys()].map(k => isum(
        f.map((fn, n) => imul(fn, expi(T * n * k)))
    ));
}

複素数の周波数成分配列Fを受け取り、複素数の時系列データを返すIDFT関数idft(F)は、以下のようになります:

[3]
function idft(F) {
    const N = F.length, T = 2 * Math.PI / N;
    return [...Array(N).keys()].map(t => isum(
        F.map((Fn, n) => imul(Fn, expi(T * n * t)))
    )).map(([r, i]) => [r / N, i / N]);
}

以下、実数値の時系列データfr0を用意し、複素数化したf0を用いて、DFT,IDFTを行う実行例です:

[4]
{
    const fr0 = [1,3,4,2, 5,6,2,4, 0,1,3,4, 5,62,2,3];
    const f0 = fr0.map(r => [r, 0]);

    const F = dft(f0);
    const f1 = idft(F);
    const fr1 = f1.map(([r]) => r);

    console.log("fr0:", fr0);
    console.log("F:", F);
    console.log("f1:", f1);
    console.log("fr1:", fr1.map(Math.round));
}
fr0: [ 1, 3, 4, 2, 5, 6, 2, 4, 0, 1, 3, 4, 5, 62, 2, 3 ]
F: [ [ 107, 0 ],
  [ 23.295891661412693, 51.72985580737281 ],
  [ -53.54772721475247, 42.96194077712561 ],
  [ -49.21391810443097, -25.674384455895552 ],
  [ -7.869471018244225e-14, -59 ],
  [ 49.79970454205781, -24.2601708935226 ],
  [ 35.54772721475254, 48.96194077712551 ],
  [ -19.8816780990393, 53.14406936974603 ],
  [ -63, 9.583546756334772e-14 ],
  [ -19.881678099039497, -53.14406936974596 ],
  [ 35.54772721475236, -48.961940777125726 ],
  [ 49.799704542057995, 24.260170893522123 ],
  [ 2.3608413054732673e-13, 58.99999999999999 ],
  [ -49.21391810443097, 25.67438445589565 ],
  [ -53.54772721475294, -42.96194077712513 ],
  [ 23.29589166141229, -51.72985580737299 ] ]
f1: [ [ 0.9999999999999822, -7.993605777301127e-15 ],
  [ 2.9999999999999187, 3.68594044175552e-14 ],
  [ 4.000000000000027, 6.17284001691587e-14 ],
  [ 2.0000000000000044, 1.9095836023552692e-14 ],
  [ 5.000000000000019, -8.659739592076221e-15 ],
  [ 5.999999999999996, -1.509903313490213e-14 ],
  [ 1.9999999999999831, 2.2426505097428162e-14 ],
  [ 3.999999999999982, 3.907985046680551e-14 ],
  [ -1.2434497875801753e-14, 8.704148513061227e-14 ],
  [ 1.000000000000095, 1.9539925233402755e-14 ],
  [ 3.0000000000000027, -1.865174681370263e-14 ],
  [ 4.000000000000051, -1.9539925233402755e-14 ],
  [ 5.000000000000107, -1.2212453270876722e-14 ],
  [ 61.99999999999999, -2.816635813474022e-13 ],
  [ 1.9999999999999196, -3.6415315207705135e-14 ],
  [ 3.000000000000007, -1.3766765505351941e-14 ] ]
fr1: [ 1, 3, 4, 2, 5, 6, 2, 4, -0, 1, 3, 4, 5, 62, 2, 3 ]

DFTしたものをIDFTし誤差を丸めたfr1は、もとの値fr0の値にもどっているのが確認できました(ただし、0の符号を除く)。

3. DFTとIDFTの計算部分の共通化

DFTとIDFTの処理の違いは、expiに渡すTの符号が違う部分と、IDFTの最後のNで割る部分だけです。

この共通部分のコードをdftc(c, T)としてまとめると、以下のようになります。

[5]
function dftc(c, T) {
    return [...Array(c.length).keys()].map(i => isum(
        c.map((cn, n) => imul(cn, expi(T * n * i)))
    ));
}

function dft(f) {
    const N = f.length, T = -2 * Math.PI / N;
    return dftc(f, T);
}
function idft(F) {
    const N = F.length, T = 2 * Math.PI / N;
    return dftc(F, T).map(([r, i]) => [r / N, i / N]);
}

FFTでも逆変換との計算部分の共通化をしたものとして、プログラムを考えていきます。

4. 高速フーリエ変換(FFT)

高速フーリエ変換は、要素数に制限を加えることによって可能となった、離散フーリエ変換の計算量の少ない実装です。
時系列データから周波数成分列データへの変換を「高速フーリエ変換(FFT)」、その逆変換を「逆高速フーリエ変換(Inverse FFT, IFFT)」と呼びます。

離散フーリエ変換DFTやその逆変換IDFは、ともに結果の各成分ごとに、N回の複素数の積を取りその総和を計算するため、その計算量はO(N^2)となります。
高速フーリエ変換(Fast Fourier Transform, FFT)では、Nが2のべき乗のときに使える手法で、変換と逆変換の計算量をともにO(NlogN)に抑える事ができます。
たとえばN=8のとき、DFTでは8x8=64回計算するところを、FFTでは8 x log2(8) = 8x3 = 24回の計算でできるようになります。

ここでは、先に再帰関数によるFFT関数の実装を与え、その実行結果を分析することによって、FFTではどういう計算方法になっているのかについて理解していきます。

5. JavaScriptでの再帰版FFT実装コード

FFTとIFFTの共通部分を再帰関数fftrec(c, T, N, s, w)で実装します。
再帰呼び出しでは、データ列を二分させて呼び出します。つまり、log2(N)段再帰され、各段のトータルはN回の計算になることで、O(NlogN)の計算になります。

  • cとTはDFTのときと同じ、データ列とexpiに渡す定数です。Tは再帰呼び出しで2倍にしていきます。
  • Nは、その再帰ステップで処理するデータ列の要素数(2のべき乗の数)で、再帰呼出しで半減させていきます。
  • sは、その最期ステップで扱うcでの先頭インデックスです。
  • wは、2の再帰段数乗(1 => 2 => 4 => 8 => ...)です。再帰の終端(N=1)のときに、インデックスsとs+wを隣り合わせにし、計算させます(たすき掛けの相手)。

sとwは、データ列cでのインデックスでの、演算で使う2つの複素数を取り出すためのデータであり、コードではわかりにくいものとなります。
このあとで説明するように、具体的なデータを使って実行した結果を見れば、これが何をするためのコードだったかについては、すぐわかるかと思います。

まずは、再帰版FFT実装コードを提示します。FFT関数をfft0(f)、IFFT関数をifft0(F)にします。

[6]
function fftrec(c, T, N, s = 0, w = 1) {
    if (N === 1) return [c[s]];
    const Nh = N / 2, Td = T * 2, wd = w * 2;
    const rec = fftrec(c, Td, Nh, s, wd).concat(fftrec(c, Td, Nh, s + w, wd));
    for (let i = 0; i < Nh; i++) {
        const l = rec[i], re = imul(rec[i + Nh], expi(T * i));
        [rec[i], rec[i + Nh]] = [iadd(l, re), isub(l, re)];
    }
    return rec;
}

function fft0(f) {
    const N = f.length, T = -2 * Math.PI / N;
    return fftrec(f, T, N);
}
function ifft0(F) {
    const N = F.length, T = 2 * Math.PI / N;
    return fftrec(F, T, N).map(([r, i]) => [r / N, i / N]);
}

複素数計算は、forループ部分でのみ行っています。
各再帰関数内ではNh回計算します。
Nhは最初はNで、再帰ごとに半減します。一方再帰呼び出しでは2回再帰呼び出しをするので、最初の1つから始まって再帰ごとに2倍の数になっていきます。
この結果、各再帰段でトータルすると、結局はどの段でもN回複素数の計算していることになります。

再帰段数はlog2(N)なので、計算量がO(NlogN)なコードとなっていることがわかるでしょう。

このfft0ifft0を、DFTと同じ、16要素の時系列データfr0で実行してみます。

[7]
{
    const fr0 = [1,3,4,2, 5,6,2,4, 0,1,3,4, 5,62,2,3];
    const f0 = fr0.map(r => [r, 0]);

    const F = fft0(f0);
    const f1 = ifft0(F);
    const fr1 = f1.map(([r]) => r);

    console.log("fr0:", fr0);
    console.log("F:", F);
    console.log("f1:", f1);
    console.log("fr1:", fr1.map(Math.round));
}
fr0: [ 1, 3, 4, 2, 5, 6, 2, 4, 0, 1, 3, 4, 5, 62, 2, 3 ]
F: [ [ 107, 0 ],
  [ 23.29589166141268, 51.729855807372815 ],
  [ -53.5477272147525, 42.961940777125584 ],
  [ -49.21391810443094, -25.67438445589562 ],
  [ 3.6127080574846916e-15, -59 ],
  [ 49.799704542057846, -24.260170893522517 ],
  [ 35.54772721475249, 48.96194077712559 ],
  [ -19.8816780990396, 53.14406936974591 ],
  [ -63, 0 ],
  [ -19.881678099039586, -53.14406936974591 ],
  [ 35.5477272147525, -48.961940777125584 ],
  [ 49.799704542057846, 24.260170893522528 ],
  [ -3.6127080574846916e-15, 59 ],
  [ -49.21391810443094, 25.67438445589561 ],
  [ -53.54772721475249, -42.96194077712559 ],
  [ 23.295891661412693, -51.729855807372815 ] ]
f1: [ [ 1, 0 ],
  [ 3.000000000000001, -1.3877787807814457e-15 ],
  [ 3.999999999999999, -1.0143540619928357e-16 ],
  [ 2, -3.0531133177191805e-16 ],
  [ 5, 0 ],
  [ 6, -1.7763568394002505e-15 ],
  [ 2, 4.592425496802574e-17 ],
  [ 3.9999999999999996, -2.498001805406602e-16 ],
  [ 0, 0 ],
  [ 0.9999999999999991, 1.3877787807814457e-15 ],
  [ 3.000000000000001, 9.586896263232085e-18 ],
  [ 4.000000000000001, 3.608224830031759e-16 ],
  [ 5, 0 ],
  [ 62, 1.7763568394002505e-15 ],
  [ 2, 4.592425496802574e-17 ],
  [ 2.9999999999999996, 1.942890293094024e-16 ] ]
fr1: [ 1, 3, 4, 2, 5, 6, 2, 4, 0, 1, 3, 4, 5, 62, 2, 3 ]

DFTのときとほぼ同様の結果となりました。

6. 再帰版FFTの実行結果を分析する

この再帰実装FFTのfftrecは、再帰呼び出しの前後で、別々の処理を行っています:

  • 前半: データの並べ替えをする
  • 後半: 複素数の計算を行う

まず、前半のデータの並べ替えを解析します。

6.1. FFT前半部: 並べ替え部分の実行結果を分析する

fftrecの並び替えの部分だけを切り出したものをreorderとして切り出したものは、以下のようになります:

[8]
function reorder(c, N = c.length, s = 0, w = 1) {
    if (N === 1) return [c[s]];
    const Nh = N / 2, wd = w * 2;
    const rec = reorder(c, Nh, s, wd).concat(reorder(c, Nh, s + w, wd));
    return rec;
}
console.log(reorder([0,1,2,3]));
console.log(reorder([0,1,2,3,4,5,6,7]));
console.log(reorder([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]));
[ 0, 2, 1, 3 ]
[ 0, 4, 2, 6, 1, 5, 3, 7 ]
[ 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 ]

この結果の[0,2,1,3]というのは、rreorderによって[c[0], c[2], c[1], c[3]]になった、ということを意味しています。

この並びは一見わかりにくい結果ですが、数値を2進数で表現することで、その意味がすぐわかるようになります。
整数をk桁2進数文字列にする関数toBin(k)(n)は、以下の通りになります。

[9]
function toBin(k) {
    return n => n.toString(2).padStart(k, "0");
}

これを使ってreorderするものと、した結果をそれぞれ2進数で表示してみます。

[10]
console.log([0,1,2,3].map(toBin(2)));
console.log(reorder([0,1,2,3]).map(toBin(2)));

console.log([0,1,2,3,4,5,6,7].map(toBin(3)));
console.log(reorder([0,1,2,3,4,5,6,7]).map(toBin(3)));

console.log([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15].map(toBin(4)));
console.log(reorder([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]).map(toBin(4)));
[ '00', '01', '10', '11' ]
[ '00', '10', '01', '11' ]
[ '000', '001', '010', '011', '100', '101', '110', '111' ]
[ '000', '100', '010', '110', '001', '101', '011', '111' ]
[ '0000',
  '0001',
  '0010',
  '0011',
  '0100',
  '0101',
  '0110',
  '0111',
  '1000',
  '1001',
  '1010',
  '1011',
  '1100',
  '1101',
  '1110',
  '1111' ]
[ '0000',
  '1000',
  '0100',
  '1100',
  '0010',
  '1010',
  '0110',
  '1110',
  '0001',
  '1001',
  '0101',
  '1101',
  '0011',
  '1011',
  '0111',
  '1111' ]

このように、reorderの再帰処理でやってることは、インデックスを2進数での前後を逆転させた位置に移したものだったのです。

次に、再帰実装ではなく、k桁2進数の順序逆転関数revBit(k, n)を実装し、それによって、reorderを再帰なしで実装してみます。

[11]
function revBit(k, n) {
    let r = 0;
    for (let i = 0; i < k; i++) r = (r << 1) | ((n >>> i) & 1);
    return r;
}
[12]
function reorder(c, N = c.length) {
    const k = Math.log2(N);
    console.assert(Number.isInteger(k), "c.length should be power of 2");
    return c.map((_, i) => c[revBit(k, i)]);
}

このコードで、再帰関数のときと同様の結果が、map(ループ)によって得られます。

整数が32bitであることを前提にすると、ビット操作でrevBitをループなしで以下のように実装できます。

[13]
function revBit(k, n0) {
    if (k === 1) return n0;
    const s1 = ((n0 & 0xaaaaaaaa) >>> 1) | ((n0 & 0x55555555) << 1);
    if (k === 2) return s1;
    const s2 = ((s1 & 0xcccccccc) >>> 2) | ((s1 & 0x33333333) << 2);
    if (k <= 4) return s2 >>> (4 - k);
    const s3 = ((s2 & 0xf0f0f0f0) >>> 4) | ((s2 & 0x0f0f0f0f) << 4);
    if (k <= 8) return s3 >>> (8 - k);
    const s4 = ((s3 & 0xff00ff00) >>> 8) | ((s3 & 0x00ff00ff) << 8);
    if (k <= 16) return s3 >>> (16 - k);
    const s5 = ((s4 & 0xffff0000) >>> 16) | ((s4 & 0x0000ffff) << 16);
    return s5 >>> (32 - k);
}

これは、"ab cd ef gh"を1つづつ入れ替えると"ba dc fe hg"になり、次に2つづつで入れ替えると"dcba hgfe"となり、
最後に4つで入れ替えると"hgfe dcba"となるのと同じやり方です。そして、返す直前にk桁に切り詰めています。

6.2. FFT後半部: 複素数計算部分の実行結果を分析する

ここでは、要素数N=8での計算について考えていきます。

まず、DFTでの計算について、N=8のときで確認します:

F(k) = f(0) \exp(\frac{-2\pi i}{8} 0 k) + f(1) \exp(\frac{-2\pi i}{8} 1 k) + 
       f(2) \exp(\frac{-2\pi i}{8} 2 k) + f(3) \exp(\frac{-2\pi i}{8} 3 k) +
       f(4) \exp(\frac{-2\pi i}{8} 4 k) + f(5) \exp(\frac{-2\pi i}{8} 5 k) +
       f(6) \exp(\frac{-2\pi i}{8} 6 k) + f(7) \exp(\frac{-2\pi i}{8} 7 k)

ここで、$E(n) = \exp(\frac{-2\pi i}{8} n)$とすると、$\exp(\frac{-2\pi i}{8} t k)$は短く、E(t * k)とかけます。
このとき、$F(k)$は以下のようになります:

F(k) = E(0 * k)f(0) + E(1 * k)f(1) + E(2 * k)f(2) + E(3 * k)f(3) + E(4 * k)f(4) + E(5 * k)f(5) + E(6 * k)f(6) + E(7 * k)f(7)

まず、この係数E(n * k)に着目し、表にすると以下のようになっています:

F\f f(0) f(1) f(2) f(3) f(4) f(5) f(6) f(7)
F(0) E(0) E(0) E(0) E(0) E(0) E(0) E(0) E(0)
F(1) E(0) E(1) E(2) E(3) E(4) E(5) E(6) E(7)
F(2) E(0) E(2) E(4) E(6) E(8) E(10) E(12) E(14)
F(3) E(0) E(3) E(6) E(9) E(12) E(15) E(18) E(21)
F(4) E(0) E(4) E(8) E(12) E(16) E(20) E(24) E(28)
F(5) E(0) E(5) E(10) E(15) E(20) E(25) E(30) E(35)
F(6) E(0) E(6) E(12) E(18) E(24) E(30) E(36) E(42)
F(7) E(0) E(7) E(14) E(21) E(28) E(35) E(42) E(49)

E(n)はべき乗でもあるから、E(n)E(m) = E(n + m)です。

また、$\exp(-2\pi i) = \exp(\frac{-2\pi i}{8} 8) = 1$、$\exp(-\pi i) = \exp(\frac{-2\pi}{8} 4) = -1$といった関係があります。
これはE(0) = E(8) = 1E(4) = -1になるので、E(n + 8) = E(n)E(8) = E(n)E(n + 4) = E(n)E(4) = -E(n)という関係を持ちます。

E(n + 8) = E(n)から、先の表のE(n)nを8で割った余りにすると以下のようになります:

F\f f(0) f(1) f(2) f(3) f(4) f(5) f(6) f(7)
F(0) E(0) E(0) E(0) E(0) E(0) E(0) E(0) E(0)
F(1) E(0) E(1) E(2) E(3) E(4) E(5) E(6) E(7)
F(2) E(0) E(2) E(4) E(6) E(0) E(2) E(4) E(6)
F(3) E(0) E(3) E(6) E(1) E(4) E(7) E(2) E(5)
F(4) E(0) E(4) E(0) E(4) E(0) E(4) E(0) E(4)
F(5) E(0) E(5) E(2) E(7) E(4) E(1) E(6) E(3)
F(6) E(0) E(6) E(4) E(2) E(0) E(6) E(4) E(2)
F(7) E(0) E(7) E(6) E(5) E(4) E(3) E(2) E(1)

この表に、先に調べた、前半でのビット前後反転した順でのデータ列の入れ替え[ 0, 4, 2, 6, 1, 5, 3, 7 ]を行うと、以下のようになります:

F\f f(0) f(4) f(2) f(6) f(1) f(5) f(3) f(7)
F(0) E(0) E(0) E(0) E(0) E(0) E(0) E(0) E(0)
F(1) E(0) E(4) E(2) E(6) E(1) E(5) E(3) E(7)
F(2) E(0) E(0) E(4) E(4) E(2) E(2) E(6) E(6)
F(3) E(0) E(4) E(6) E(2) E(3) E(7) E(1) E(5)
F(4) E(0) E(0) E(0) E(0) E(4) E(4) E(4) E(4)
F(5) E(0) E(4) E(2) E(6) E(5) E(1) E(7) E(3)
F(6) E(0) E(0) E(4) E(4) E(6) E(6) E(2) E(2)
F(7) E(0) E(4) E(6) E(2) E(7) E(3) E(5) E(1)

ここで、E(n)のnだけ並べてみます。

F\f 0 4 2 6 1 5 3 7
0 0 0 0 0 0 0 0 0
1 0 4 2 6 1 5 3 7
2 0 0 4 4 2 2 6 6
3 0 4 6 2 3 7 1 5
4 0 0 0 0 4 4 4 4
5 0 4 2 6 5 1 7 3
6 0 0 4 4 6 6 2 2
7 0 4 6 2 7 3 5 1

この並び替えによって、F(0)とF(4)のあいだなどで、共通部分のあるパターンがなんとなく見えてきたかもしれません。
次は、このパターンについて見ていきます。


先に示したE(n + 8) = E(n)E(n + 4) = -E(n)より、E(n)には:

  • -E(0) = E(4)
  • -E(1) = E(5)
  • -E(2) = E(6)
  • -E(3) = E(7)

かつ:

  • -E(4) = E(0)
  • -E(5) = E(1)
  • -E(6) = E(2)
  • -E(7) = E(3)

の関係があります。つまりE(n)のnには、0と4、1と5、2と6、3と7は、互いにマイナスをかけた関係にあります。

ここからたとえば、F(0)とF(4)は、F(0)の前側の0426部分をL0、後ろ側の1537部分をR0とすると、

  • F(0) = L0 + R0
  • F(4) = L0 - R0 = L0 + E(4)R0

という関係になっています。同様に、

  • F(1) = L1 + R1
  • F(5) = L1 - R1
  • F(2) = L2 + R2
  • F(6) = L2 - R2
  • F(3) = L3 + R3
  • F(7) = L3 - R3

ともなっています。


最後に、このLRの計算を、再帰の一番奥の段から、足し合わせていくときの計算ステップを具体的に並べていきます。

再帰版FFTの後半の計算コードは、同一段数の再帰処理でまとめることで、各再帰段ごとで全要素を1回づつ更新しています。
(実際の動作では、葉がN要素な二分木での深さ優先の帰りがけ順で、各段としてはバラバラに計算されます。
しかし、二分木のそれぞれの枝での計算範囲は完全に分割されているので、幅優先で処理しても同じ結果になります。)
N=8では、Nを半減させてN=1になるまで、3段の(2回づつの)再帰呼び出しが、再帰関数中で行われることになります。

以下は、この再帰段数を一度に巻き戻すことの順に、計算される内容と結果を列挙したものです。
入力値の時系列データの値は、それぞれf(0) = f0f(1) = f1、 ...、f(7) = f7と記述します。

再帰段3

前半の入れ替え直後の初期値です。

  • F0(0) = f0
  • F0(1) = f4
  • F0(2) = f2
  • F0(3) = f6
  • F0(4) = f1
  • F0(5) = f5
  • F0(6) = f3
  • F0(7) = f7

再帰段2

Nhが1、つまりループはi = 0のみで、ループ中の複素数計算は:

  • L1 = L0 + E(i*4)R0 = E(0)L0 + E(i*4+0)R0
  • R1 = L0 - E(i*4)R0 = E(0)L0 + E(i*4+4)R0

より、以下の計算が行われます:

  • s = 0, i = 0
    • F1(0) = F0(0) + F0(1) = E(0)f0 + E(0)f4
    • F1(1) = F0(0) - F0(1) = E(0)f0 + E(4)f4
  • s = 2, i = 0
    • F1(2) = F0(2) + F0(3) = E(0)f2 + E(0)f6
    • F1(3) = F0(2) - F0(3) = E(0)f2 + E(4)f6
  • s = 4, i = 0
    • F1(4) = F0(4) + F0(5) = E(0)f1 + E(0)f5
    • F1(5) = F0(4) - F0(5) = E(0)f1 + E(4)f5
  • s = 6, i = 0
    • F1(6) = F0(6) + F0(7) = E(0)f3 + E(0)f7
    • F1(7) = F0(6) - F0(7) = E(0)f3 + E(4)f7

この結果の値を順に並べると:

  • F1(0) = E(0)f0 + E(0)f4
  • F1(1) = E(0)f0 + E(4)f4
  • F1(2) = E(0)f2 + E(0)f6
  • F1(3) = E(0)f2 + E(4)f6
  • F1(4) = E(0)f1 + E(0)f5
  • F1(5) = E(0)f1 + E(4)f5
  • F1(6) = E(0)f3 + E(0)f7
  • F1(7) = E(0)f3 + E(4)f7

となります。


再帰段1

Nhが2,つまりループはi = 0,1で、ループ中の複素数計算は:

  • L2 = L1 + E(i*2)R1 = E(0)L1 + E(i*2)R1
  • R2 = L1 - E(i*2)R1 = E(0)L1 + E(i*2+4)R1

より、以下の計算が行われます:

  • s = 0, i = 0
    • F2(0) = F1(0) + E(0)F1(2) = E(0)f0 + E(0)f4 + E(0)f2 + E(0)f6
    • F2(2) = F1(0) - E(0)F1(2) = E(0)f0 + E(0)f4 + E(4)f2 + E(4)f6
  • s = 0, i = 1
    • F2(1) = F1(1) + E(2)F1(3) = E(0)f0 + E(4)f4 + E(2)f2 + E(6)f6
    • F2(3) = F1(1) - E(2)F1(3) = E(0)f0 + E(4)f4 + E(6)f2 + E(2)f6
  • s = 4, i = 0
    • F2(4) = F1(4) + E(0)F1(6) = E(0)f1 + E(0)f5 + E(0)f3 + E(0)f7
    • F2(6) = F1(4) - E(0)F1(6) = E(0)f1 + E(0)f5 + E(4)f3 + E(4)f7
  • s = 4, i = 1
    • F2(5) = F1(5) + E(2)F1(7) = E(0)f1 + E(4)f5 + E(2)f3 + E(6)f7
    • F2(7) = F1(5) - E(2)F1(7) = E(0)f1 + E(4)f5 + E(6)f3 + E(2)f7

この結果の値を順に並べると:

  • F2(0) = E(0)f0 + E(0)f4 + E(0)f2 + E(0)f6
  • F2(1) = E(0)f0 + E(4)f4 + E(2)f2 + E(6)f6
  • F2(2) = E(0)f0 + E(0)f4 + E(4)f2 + E(4)f6
  • F2(3) = E(0)f0 + E(4)f4 + E(6)f2 + E(2)f6
  • F2(4) = E(0)f1 + E(0)f5 + E(0)f3 + E(0)f7
  • F2(5) = E(0)f1 + E(4)f5 + E(2)f3 + E(6)f7
  • F2(6) = E(0)f1 + E(0)f5 + E(4)f3 + E(4)f7
  • F2(7) = E(0)f1 + E(4)f5 + E(6)f3 + E(2)f7

となります。


再帰段0

Nhが4、つまりループはi = 0,1,2,3で、ループ中の複素数計算は:

  • L3 = L2 + E(i*1)R2 = E(0)L2 + E(i*1)R2
  • R3 = L2 - E(i*1)R2 = E(0)L2 + E(i*1+4)R2

より、以下の計算が行われます:

  • s = 0, i = 0
    • F3(0) = F2(0) + E(0)F2(4) = E(0)f0 + E(0)f4 + E(0)f2 + E(0)f6 + E(0)f1 + E(0)f5 + E(0)f3 + E(0)f7
    • F3(4) = F2(0) - E(0)F2(4) = E(0)f0 + E(0)f4 + E(0)f2 + E(0)f6 + E(4)f1 + E(4)f5 + E(4)f3 + E(4)f7
  • s = 0, i = 1
    • F3(1) = F2(1) + E(1)F2(5) = E(0)f0 + E(4)f4 + E(2)f2 + E(6)f6 + E(1)f1 + E(5)f5 + E(3)f3 + E(7)f7
    • F3(5) = F2(1) - E(1)F2(5) = E(0)f0 + E(4)f4 + E(2)f2 + E(6)f6 + E(5)f1 + E(1)f5 + E(7)f3 + E(3)f7
  • s = 0, i = 2
    • F3(2) = F2(2) + E(2)F2(6) = E(0)f0 + E(0)f4 + E(4)f2 + E(4)f6 + E(2)f1 + E(2)f5 + E(6)f3 + E(6)f7
    • F3(6) = F2(2) - E(2)F2(6) = E(0)f0 + E(0)f4 + E(4)f2 + E(4)f6 + E(6)f1 + E(6)f5 + E(2)f3 + E(2)f7
  • s = 0, i = 3
    • F3(3) = F2(3) + E(3)F2(7) = E(0)f0 + E(4)f4 + E(6)f2 + E(2)f6 + E(3)f1 + E(7)f5 + E(1)f3 + E(5)f7
    • F3(7) = F2(3) - E(3)F2(7) = E(0)f0 + E(4)f4 + E(6)f2 + E(2)f6 + E(7)f1 + E(3)f5 + E(5)f3 + E(1)f7

この結果の値を順に並べると:

  • F3(0) = E(0)f0 + E(0)f4 + E(0)f2 + E(0)f6 + E(0)f1 + E(0)f5 + E(0)f3 + E(0)f7
  • F3(1) = E(0)f0 + E(4)f4 + E(2)f2 + E(6)f6 + E(1)f1 + E(5)f5 + E(3)f3 + E(7)f7
  • F3(2) = E(0)f0 + E(0)f4 + E(4)f2 + E(4)f6 + E(2)f1 + E(2)f5 + E(6)f3 + E(6)f7
  • F3(3) = E(0)f0 + E(4)f4 + E(6)f2 + E(2)f6 + E(3)f1 + E(7)f5 + E(1)f3 + E(5)f7
  • F3(4) = E(0)f0 + E(0)f4 + E(0)f2 + E(0)f6 + E(4)f1 + E(4)f5 + E(4)f3 + E(4)f7
  • F3(5) = E(0)f0 + E(4)f4 + E(2)f2 + E(6)f6 + E(5)f1 + E(1)f5 + E(7)f3 + E(3)f7
  • F3(6) = E(0)f0 + E(0)f4 + E(4)f2 + E(4)f6 + E(6)f1 + E(6)f5 + E(2)f3 + E(2)f7
  • F3(7) = E(0)f0 + E(4)f4 + E(6)f2 + E(2)f6 + E(7)f1 + E(3)f5 + E(5)f3 + E(1)f7

となります。

このE(n)を表にすると、

F3\ft f0 f4 f2 f6 f1 f5 f3 f7
F3(0) E(0) E(0) E(0) E(0) E(0) E(0) E(0) E(0)
F3(1) E(0) E(4) E(2) E(6) E(1) E(5) E(3) E(7)
F3(2) E(0) E(0) E(4) E(4) E(2) E(2) E(6) E(6)
F3(3) E(0) E(4) E(6) E(2) E(3) E(7) E(1) E(5)
F3(4) E(0) E(0) E(0) E(0) E(4) E(4) E(4) E(4)
F3(5) E(0) E(4) E(2) E(6) E(5) E(1) E(7) E(3)
F3(6) E(0) E(0) E(4) E(4) E(6) E(6) E(2) E(2)
F3(7) E(0) E(4) E(6) E(2) E(7) E(3) E(5) E(1)

となり、先の表と同じ値となっていることがわかります。

7. JavaScriptによるループ版FFT実装コード

上述の再帰段数ごとの計算を、要素数が任意の2のべき乗数Nとして、ループによって直接的に実装したものが、ループ版FFTとなります。

FFTとIFFTの共通処理部分をfftin(c, T, N)としました。ただし、引数Tの値は再帰版と違い、Nで割っていません。

最内ループ内のコードでの変数(i, Nh, l, re)は、再帰版FFTでのループ処理での変数と合わせてあります。

[14]
function fftin(c, T, N) {
    const k = Math.log2(N);
    const rec = c.map((_, i) => c[revBit(k, i)]);
    for (let Nh = 1; Nh < N; Nh *= 2) {
        T /= 2;
        for (let s = 0; s < N; s += Nh * 2) {
            for (let i = 0; i < Nh; i++) {
                const l = rec[s + i], re = imul(rec[s + i + Nh], expi(T * i));
                [rec[s + i], rec[s + i + Nh]] = [iadd(l, re), isub(l, re)];
            }
        }
    }
    return rec;
}

function fft1(f) {
    const N = f.length, T = -2 * Math.PI;
    return fftin(f, T, N);
}
function ifft1(F) {
    const N = F.length, T = 2 * Math.PI;
    return fftin(F, T, N).map(([r, i]) => [r / N, i / N]);
}

最外ループでは、Nhを倍々にしていくことから、log2(N)回実行されます。
その中のループは2つ合わせて、中の計算部分をN/2回実行するものとなります。

この実装で、再帰版FFTのときと同じfr0を使って計算してみましょう。

[15]
{
    const fr0 = [1,3,4,2, 5,6,2,4, 0,1,3,4, 5,62,2,3];
    const f0 = fr0.map(r => [r, 0]);

    const F = fft1(f0);
    const f1 = ifft1(F);
    const fr1 = f1.map(([r]) => r);

    console.log("fr0:", fr0);
    console.log("F:", F);
    console.log("f1:", f1);
    console.log("fr1:", fr1.map(Math.round));
}
fr0: [ 1, 3, 4, 2, 5, 6, 2, 4, 0, 1, 3, 4, 5, 62, 2, 3 ]
F: [ [ 107, 0 ],
  [ 23.29589166141268, 51.729855807372815 ],
  [ -53.5477272147525, 42.961940777125584 ],
  [ -49.21391810443094, -25.67438445589562 ],
  [ 3.6127080574846916e-15, -59 ],
  [ 49.799704542057846, -24.260170893522517 ],
  [ 35.54772721475249, 48.96194077712559 ],
  [ -19.8816780990396, 53.14406936974591 ],
  [ -63, 0 ],
  [ -19.881678099039586, -53.14406936974591 ],
  [ 35.5477272147525, -48.961940777125584 ],
  [ 49.799704542057846, 24.260170893522528 ],
  [ -3.6127080574846916e-15, 59 ],
  [ -49.21391810443094, 25.67438445589561 ],
  [ -53.54772721475249, -42.96194077712559 ],
  [ 23.295891661412693, -51.729855807372815 ] ]
f1: [ [ 1, 0 ],
  [ 3.000000000000001, -1.3877787807814457e-15 ],
  [ 3.999999999999999, -1.0143540619928357e-16 ],
  [ 2, -3.0531133177191805e-16 ],
  [ 5, 0 ],
  [ 6, -1.7763568394002505e-15 ],
  [ 2, 4.592425496802574e-17 ],
  [ 3.9999999999999996, -2.498001805406602e-16 ],
  [ 0, 0 ],
  [ 0.9999999999999991, 1.3877787807814457e-15 ],
  [ 3.000000000000001, 9.586896263232085e-18 ],
  [ 4.000000000000001, 3.608224830031759e-16 ],
  [ 5, 0 ],
  [ 62, 1.7763568394002505e-15 ],
  [ 2, 4.592425496802574e-17 ],
  [ 2.9999999999999996, 1.942890293094024e-16 ] ]
fr1: [ 1, 3, 4, 2, 5, 6, 2, 4, 0, 1, 3, 4, 5, 62, 2, 3 ]

結果は、再帰版FFTのときと同じ値になりました。

8. まとめ

DFTの実装の理解を前提に、再帰版FFTの実装を提示し、再帰版FFTの振る舞いを具体例で解析することで、FFTの仕組みを解明し、最後にその仕組をループで直接実行したループ版FFTを作成しました。

再帰版FFTの前半部の解析では、インデックスの2進数表現を用いました。

内部で2度自身を呼び出す再帰関数は、O(NlogN)な処理として、葉がN要素の完全二分木のデータ構造への処理を行っているとみなせます。
この隠れた二分木を表出させるのが、再帰で扱う数値の2進数表現になります。

たとえば、ハノイの塔は、この内部で二度自身を呼び出す再帰関数になります。
これも2進数表現化を応用することで、固定段のループによって実装させることができます。

付録: 任意長データのためのFFT

2のべき乗のFFT/IFFTを用いて、任意長のデータを扱うFFTを作ることができます。
この任意長FFTは、Bluestein's algorithmと呼ばれています。

2つの数列(関数)をかけ合わせて総和(積分)を取ることを、畳み込み(convolution)といいます。
フーリエ変換やラプラス変換は、べき乗タイプ($\exp(C x)$)の数列(関数)との畳み込みを行っています。
このタイプの畳み込みによる変換では、2つの数列(関数)に対して、変換した後同士の積をとり、その逆変換をすると、もとの2数列(関数)の畳み込みになっている、という性質があります。

Bluestein's algorithmでは、DFTの$\exp(\frac{-2\pi i}{N}n k)$を分解して、その畳み込みに変形します。
この変形後の畳み込みは、2のべき乗長にデータを拡大して0パディングするのに適した形になります。

まずDFTの畳み込みの式が、リンク先での$a_n$と$b_n$との畳込みの式へと変換されます。
そこから、この$a_n$と$b_n$を、2のべき乗長に拡大します。
$a_n$は後ろを単に0で埋めるだけですが、$b_n$のほうは後ろからも反転したものを入れます(偶関数になる)。

この2のべき乗長に拡大された$a_n$と$b_n$の畳み込みを、2のべき乗長のFFTとIFFTを使って行い、その結果から、先頭からもとの長さだけ切り出し、$b$の複素共役とかけることで、DFTでの結果とほぼ同じものになる、という仕組みです。


このBluestein's algorithmのコードは以下のようになります(fft2(f)ifft2(F)):

[16]
function conj([x, y]) {return [x, -y];}

function fftin2(c, T, N) {
    const Nd = N * 2;
    const bc = c.map((_, n) => expi(T * n * n / Nd));
    const b = bc.map(conj);
    const a = c.map((cn, n) => imul(cn, bc[n]));

    //const N2 = 1 << Math.ceil(Math.log2(Nd - 1));
    const N2 = 1 << (32 - Math.clz32(Nd - 1));
    const a2 = a.concat(Array(N2 - N).fill([0, 0]));
    const b2 = b.concat(Array(N2 - Nd + 1).fill([0, 0]), b.slice(1).reverse());
    const A2 = fft1(a2);
    const B2 = fft1(b2);
    const AB2 = A2.map((A2n, n) => imul(A2n, B2[n]));
    const ab2 = ifft1(AB2);
    return bc.map((bcn, n) => imul(bcn, ab2[n]));
}

function fft2(f) {
    const N = f.length, T = -2 * Math.PI; 
    return fftin2(f, T, N);
}
function ifft2(F) {
    const N = F.length, T = 2 * Math.PI;
    return fftin2(F, T, N).map(([x, y]) => [x / N, y / N]);
}
[17]
{
    // 15 elements example
    const fr0 = [1,3,4,2, 5,6,2,4, 0,1,3,4, 5,62,2];
    const f0 = fr0.map(r => [r, 0]);

    const F = fft2(f0);
    const f1 = ifft2(F);
    const fr1 = f1.map(([r]) => r);

    console.log("fr0:", fr0);
    console.log("F:", F);
    console.log("f1:", f1);
    console.log("fr1:", fr1.map(Math.round));
}
fr0: [ 1, 3, 4, 2, 5, 6, 2, 4, 0, 1, 3, 4, 5, 62, 2 ]
F: [ [ 104.00000000000001, 1.4210854715202004e-14 ],
  [ 40.113068713053366, 40.536802661213386 ],
  [ -16.938440097404285, 64.08647185352932 ],
  [ -47.041019662496865, 29.02599135062092 ],
  [ -57.94588444305084, -15.35101781191083 ],
  [ -35.500000000000064, -52.82754963085073 ],
  [ 20.04101966249682, -49.09166758334534 ],
  [ 52.77125582740197, -26.917243683390566 ],
  [ 52.771255827401916, 26.91724368339068 ],
  [ 20.041019662496826, 49.091667583345306 ],
  [ -35.49999999999999, 52.82754963085077 ],
  [ -57.94588444305086, 15.351017811910662 ],
  [ -47.04101966249681, -29.02599135062106 ],
  [ -16.938440097404428, -64.08647185352933 ],
  [ 40.11306871305353, -40.53680266121326 ] ]
f1: [ [ 1.0000000000000246, -8.763360407707903e-15 ],
  [ 3.0000000000000235, -1.4092430925908654e-14 ],
  [ 4.000000000000012, -6.158037043254202e-15 ],
  [ 2.000000000000004, 6.2764608325475514e-15 ],
  [ 4.999999999999981, 5.921189464667502e-15 ],
  [ 5.999999999999992, -2.0368891758456206e-14 ],
  [ 2.0000000000000098, -8.052817671947802e-15 ],
  [ 4.000000000000021, 9.473903143468002e-16 ],
  [ 1.1817018103590317e-14, 9.119920035791959e-15 ],
  [ 0.9999999999999932, 1.0835776720341527e-14 ],
  [ 2.9999999999999916, -1.4210854715202004e-14 ],
  [ 3.999999999999985, 5.8027656753741514e-15 ],
  [ 4.9999999999999964, 8.763360407707903e-15 ],
  [ 62.00000000000001, 7.579122514774402e-15 ],
  [ 2.000000000000005, 1.4802973661668755e-14 ] ]
fr1: [ 1, 3, 4, 2, 5, 6, 2, 4, 0, 1, 3, 4, 5, 62, 2 ]

比較対象に、同じデータのDFTでの結果も載せておきます:

[18]
{
    // 15 elements example
    const fr0 = [1,3,4,2, 5,6,2,4, 0,1,3,4, 5,62,2];
    const f0 = fr0.map(r => [r, 0]);

    const F = dft(f0);
    const f1 = idft(F);
    const fr1 = f1.map(([r]) => r);

    console.log("fr0:", fr0);
    console.log("F:", F);
    console.log("f1:", f1);
    console.log("fr1:", fr1.map(Math.round));
}
fr0: [ 1, 3, 4, 2, 5, 6, 2, 4, 0, 1, 3, 4, 5, 62, 2 ]
F: [ [ 104, 0 ],
  [ 40.1130687130533, 40.53680266121345 ],
  [ -16.93844009740454, 64.08647185352929 ],
  [ -47.04101966249686, 29.02599135062094 ],
  [ -57.945884443050794, -15.351017811910953 ],
  [ -35.49999999999983, -52.827549630850896 ],
  [ 20.041019662496918, -49.09166758334529 ],
  [ 52.771255827402115, -26.917243683390264 ],
  [ 52.77125582740182, 26.917243683390836 ],
  [ 20.041019662496755, 49.09166758334532 ],
  [ -35.50000000000034, 52.82754963085049 ],
  [ -57.94588444305084, 15.351017811910584 ],
  [ -47.04101966249679, -29.025991350621084 ],
  [ -16.938440097404428, -64.08647185352936 ],
  [ 40.113068713053934, -40.536802661212924 ] ]
f1: [ [ 1.0000000000000286, 1.0894988614988204e-14 ],
  [ 3.000000000000027, 1.8947806286936004e-14 ],
  [ 4.000000000000055, 1.5158245029548804e-14 ],
  [ 2.0000000000000298, -6.015928496102182e-14 ],
  [ 5.000000000000037, -8.052817671947802e-15 ],
  [ 6.000000000000018, -5.6369723703634616e-14 ],
  [ 1.9999999999999796, -6.987003568307652e-14 ],
  [ 3.9999999999999383, -3.789561257387201e-15 ],
  [ -1.7289873236829103e-14, -1.5158245029548804e-14 ],
  [ 0.9999999999999837, -1.8947806286936004e-14 ],
  [ 2.999999999999985, 1.8000415972589204e-14 ],
  [ 3.999999999999993, 3.600083194517841e-14 ],
  [ 5.000000000000014, -1.3026616822268503e-14 ],
  [ 62, -2.2737367544323207e-14 ],
  [ 1.999999999999967, -1.3263464400855204e-14 ] ]
fr1: [ 1, 3, 4, 2, 5, 6, 2, 4, -0, 1, 3, 4, 5, 62, 2 ]

参考: このドキュメントの編集環境

このドキュメントは、jupyter-notebook形式で記述したもので、ijavascriptカーネルを用いて、nodejsによってドキュメント内のコードが実行できます。
エディタ環境として、ブラウザを用いるJupyterLabを使いました。

jupyterインストール:

$ pip3 install --upgrade jupyter

ijavascriptインストールとnodejsカーネルのインストール(macの場合):

$ brew install zeromq
$ npm install -g ijavascript --zmq-external
$ ijsinstall

注: zeromqのネイティブモジュールを含むので、nodejsを更新した場合、再度npm installする必要がでるかもしれません。

JupyterLabインストールと実行:

$ pip3 install --upgrade jupyterlab
$ jupyter-lab

成功すればブラウザタブが開き、ipynbファイルの編集を行えます。

追加: ipynbからqiitaのmarkdownにしてqiitaへ貼り付ける

この本文を記述したipynbは以下のgistへ置いてあります。

jupyter-nbconvertコマンドを使えば、ipynb形式のファイルをmarkdwon形式のファイルへ変換できます:

$ jupyter-nbconvert --to markdown understanding-fft.ipynb
[NbConvertApp] Converting notebook understanding-fft.ipynb to markdown
[NbConvertApp] Writing 25680 bytes to understanding-fft.md

macosでは、このmarkdownファイルをpbcopyコマンドでクリップボードに載せ、ブラウザ上で貼り付けられます:

$ cat understanding-fft.ipynb | pbcopy

ただし、いくつかの点でqiitaとjupyter-notebookでのmarkdownには、解釈に違いがあります:

  • 改行の解釈: qiitaは改行ごとに折り返されるが、jupyter-notebookは空行で改段落になる
  • 表の解釈: qiitaでは行頭の|の前に空白を入れられない(装飾可能なテキストブロック扱いになる)が、jupyter-notebookでは行頭の|の前に空白を入れても表として処理される
  • $$な数式ブロックの解釈: qiitaでは$$なブロックでは\begin{align}環境は解釈されず数式扱いもされなくなるが、jupyter-notebookでは解釈される

この対応のために、ipynbのmarkdownで使った$$な数式ブロック部分は、mathコードブロックに書き換えるなど、の後処理が必要となるでしょう。

ここではさらに、javascriptコードブロックに、qiitaのコードファイル名表示機能を使って、実行順の:[0]をつけておきました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.useEffectで非同期処理をする場合の注意点2つ

はじめに

React 16.8から導入されたhooksにはuseEffectがあります。

詳細は公式サイトをまず参照しましょう。

useEffectを使うと、コンポーネントのレンダリングとは別に処理を書くことができます。useEffectでしばしば非同期処理を書くことがあります。例えば、サーバからのデータ取得の処理などがあります。

以下では、useEffectで非同期処理を書く場合の注意点を2つ紹介します。ケースによっては注意点はこの2つだけではない可能性が高いので、ご留意ください。

promiseを返さない

useEffectに渡す関数の戻り値はcleanup関数です。

useEffect(() => {
  console.log('side effect!');
  const cleanup = () => {
    console.log('cleanup!');
  };
  return cleanup;
}, []);

cleanup関数は次のeffectが呼ばれる前やアンマウントする場合に呼ばれます。(depsが[]なのでこの例では後者のみ)

よって、下記は間違いです。

useEffect(async () => {
  await new Promise(r => setTimeout(r, 1000));
  console.log('side effect!');
}, []);

このコードはcleanup関数の代わりにpromiseを返してしまっています。
正しくは、下記のようにします。

const sleep = ms => new Promise(r => setTimeout(r, ms));

useEffect(() => {
  const f = async () => {
    await new Promise(r => setTimeout(r, 1000));
    console.log('side effect!');
  };
  f();
}, []);

アンマウントのフラグを持つ

非同期処理を書く場合、コンポーネントが削除された後にコールバックが呼ばれる場合があります。この時、コンポーネントのステートを変更しようとするとワーニングがでます。

const [count, setCount] = useState(0);
useEffect(() => {
  const f = async () => {
    await new Promise(r => setTimeout(r, 1000));
    setCount(c => c + 1);
  };
  f();
}, []);

これを回避するには次のようにアンマウントのフラグを持ちます。

const [count, setCount] = useState(0);
useEffect(() => {
  let unmounted = false;
  const f = async () => {
    await new Promise(r => setTimeout(r, 1000));
    if (!unmounted) {
      setCount(c => c + 1);
    }
  };
  f();
  const cleanup = () => {
    unmounted = true;
  };
  return cleanup;
}, []);

おわりに

React HooksのuseEffectについて非同期処理を使う場合のよくあるケースの注意点について紹介しました。React Hooksはまだベストプラクティスが溜まっていないため、今後違う方法が主流になる可能性はある点についてはご注意ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[2日目]HTML&CSS 2週目 JS1週目

JS 1週目 Ⅲまで修了
HTML&CSS 2週目 初級終わり

2019/04/17 14:00~17:00(3h)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

長押しボタンを作ってみた【マルチブラウザ対応】

 はじめに

前回作成した記事の続きです。
長押しボタンを作ってみた【円プログレス対応】

今回やりたいこと

前回、 conic-gradient を使って長押しの進捗を表現したのですが、この関数がChrome以外のブラウザだと、利用できないため、それをなんとかします。

調べる

conic-gradient をググると以下の記事がでてきます。
CSS4のconic-gradientを先取りする方法

こちらの記事を読んでみるとpolyfillが存在するらしいので、まずはそれを使ってみます。
CSS conic-gradient() polyfill

polyfillの導入

導入方法は、 polyfill用のjsを読み込ませるだけです。

<script src="https://cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.min.js"></script>
<script src="conic-gradient.js"></script>

conic-gradient.jsは、こちらに落ちているので、落として、読み込ませれば大丈夫です。

Edgeで動かない

ブラウザのコンソールに、以下エラーがでます。

SCRIPT1014: SCRIPT1014: Invalid character

conic-gradient.js内で使われているπとかτとかが、Edgeだとダメみたいです。
なので、πpaiτtauεepsilonに書き換えました。

これで動きました。

で、css部分を確認してみると、
background-imageとしてjs内で動的に作成したsvgを設定しているみたいです。

svgデータの準備

polyfillの処理でcssに設定されているconic-gradientbackground-imageに変換しているぽいのですが、
これを連続でリアルタイムに変換する方法がわからないので、あらかじめ、svgのデータを準備して、それをレンダリングする方法を考えました。

また、そのために必要なsvgデータを準備しました。

svg.js

20個のsvgデータ保持しているjsです。色は黒のみ。window.SMART_UIというグローバル変数に格納してあります。

レンダリング処理

index.js
・・・
if (ua.indexOf('firefox') > 0 || ua.indexOf('edge') > 0 || ua.indexOf('trident') > 0) {
  var svg = 'url("' + SMART_UI.black[0] + '")';
  outerCircle.style.backgroundImage = svg;
} else {
  outerCircle.style.background = 'conic-gradient(black 0deg,white 0deg 360deg)';
}
・・・
progress = progress + (360 / (LIMIT / 100));
var outerCircle = document.getElementById('outer-circle');
if (ua.indexOf('firefox') > 0 || ua.indexOf('edge') > 0 || ua.indexOf('trident') > 0) {
  var svg = 'url("' + SMART_UI.black[progress/18] + '")';
  outerCircle.style.backgroundImage = svg;
} else {
  outerCircle.style.background = 'conic-gradient(red ' + progress + 'deg, white 0deg 360deg)';
} 
・・・

こんな感じでbackground-imageに動的にsvgを設定するようにしました。

完成

https://www.youtube.com/watch?v=gqiTLU8EBUE&t=2538

若干チラチラしますが、一応動きました。

ソースコードは、以下にあります。
https://github.com/takuhou/smart-ui/blob/feature/apply-multi-browser/index.html

今後の対応として、ライブラリ化します。

今回の記事で書いたプログラミングの様子をYoutube上にアップロードしておりますので、是非ご覧ください。
【実況】長押しボタンをプログラミング【ブラウザ対応その1】
【実況】長押しボタンをプログラミング【ブラウザ対応その2】
えんじにぁ〜TV

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

述語論理をやろうず

概要

プログラミング言語上で 1階述語論理の言語を実装したい。具体的には「論理式を 冠頭標準形 に変形し スコーレム標準形 を求め、節形式にする手順を実装に落とし込みたい」。そのために必要な 各種検討項目をまとめた。要するに 本稿は 論理プログラミング一歩手前までの数理論理学をプログラムに落とし込むための設計ドキュメントだといってよい。コードは未記載(分量が多いのでQiitaじゃないところに公開するかも)。

想定動作環境: Node上でのJavaScript で動作。依存パッケージは moo(字句解析), nearley (構文解析) のみ。npm i --save moo nearley で環境構築完了。

0. 表記 ~言語の記号~

  • 変数は以下が使える: x, y, z, w, x1, x2, ...x99
  • 定数は以下が使える: a, b, c, a1, a2, ... a99
  • 関数は以下が使える: f, g, h, f1, f2, ....f99
  • 述語は以下が使える: F, G, H, F1, F2, ..., F99
  • 括弧とカンマが使える: (, ), ,
  • 論理記号は以下が使える:
    • ¬として !, ⋀として &, ⋁として |, →として ->, ↔として ::
    • ∀xとして Ax., ∃xとして Ex.
    • = として =

論理式の例をいくつか挙げる

  • 原子論理式(コードでは prime と呼ぶ)
    • F(a), G(a13), F(a,x), G(f(a),b)
    • a=b, x=f(a,g(b)), f(a1,b)=g(a,b,c)
  • 複合論理式(コードでは compound と呼ぶ)
    • F(a)->G(b)->F(a), F(a)&G(f(b,c)), !F(a)&G(f(b)),
    • Ax.Ey.(x=y -> f(x)=f(y))

1. 内部表現

  • 構文木は S式を模した Array で表現する
    • f(a, b, c)[f, a, b, c]
    • a=b->F(a)[->, [=, a, b], [F, a]]
  • 言語の記号は文字列ではなく Object で表現する
    • x{ type: 'variable', value: 'x' }
    • a{ type: 'constant', value: 'a' }
    • f{ type: 'function', value: 'f' }
    • F{ type: 'predicate', value: 'F' }
    • ={ type: 'predicate', value: '=', infix: true }
    • !{ type: 'connective', value: 'not' }
    • &{ type: 'connective', value: 'and', infix: true }
    • |{ type: 'connective', value: 'or', infix: true }
    • ->{ type: 'connective', value: 'then', infix: true }
    • Ax.{ type: 'connective', value: 'forAll', boundID: 'x' }
    • Ex.{ type: 'connective', value: 'exists', boundID: 'x' }

幾つかの特記事項:

  • infix属性で中間記法であることを明示する。構文木を文字列にする際にのみ必要なので内部表現として保持しないという選択肢もあるが、この方式の方が論理記号の追加に対して対応しやすい(ファクトリ関数の修正だけで済む)。
  • 量化子は、束縛する変数名を文字列で持つ(boundID)。Objectで持たないということを明示するために属性名にIDを付けている。
  • 言語に追加論理記号を足す(例えば +, -, <, e, 0, ...)のは簡単。どうすればよいかを考えてみるとよい。

よって以下を実装すること:

  • toObj: 文字列を受け取り上述のオブジェクトを返す関数

2. 統語的な基本操作

一番最初に必要な 統語的な操作を記載:

  • parse: 文字列を構文木に変更する。moo, nearley, 文法ファイル, toObj を組み合わせて作る
  • repr: (人間のために)構文木を文字列にする
  • createNode: 構文木を入力として少し変形した構文木を作成する(例: 論理式の否定を作りたい, 二つの論理式の ⋀ をとった論理式を作りたい)。色々な場面で必要な操作なので まとめておくと読みやすいしコードの重複削減に貢献する。

加えて記号, 変数についての操作が必要

  • occurenceOf: 論理式の集合を渡し, 利用されている変数・定数・関数・述語の一覧を取得する。一覧が欲しいというよりも 未使用の記号が知りたいというニーズに応えることが多い。束縛変数をリネームしたい時や スコーレム関数に利用する記号を選択するなど。
  • freeOf, bndOf: 論理式における自由変数と束縛変数の一覧を求める。
  • rearrange: (人間のために) 論理式に登場する非自由な変数を機械的に一括して変更する。(例: Ax12.Ez.x12=f(x23,a, z)Ax.Ey.x=f(x23,a,y) に変更)。純統語的な操作ではないが正当化できる。自由変数をリネームしないのは、他の論理式との兼ね合いがあるため。注: 後述の束縛変数のリネームとは本質的に異なる操作 (例: F(z)->Az.F(z)を考えてみよ)

再帰・帰納の時によく使う操作:

  • isPrimeFormula: 与えられた論理式が原子論理式であることを判定。再帰の終了条件の判定に頻出。
  • isOpenFormula: 与えられた論理式に量化子が入っていないことを判定。再帰の終了条件の判定に頻出(特にPNF, SNFまわり)。

3. 統語的な応用操作

統語的な操作ではあるが、その定義に意味論的・証明論的なバックグラウンドを持つ各種操作をまとめる。

  • boundRename: 指定した束縛変数名を変更する操作。α同値性がバックグラウンドになっている(例: F(z) -> Az.F(z)F(z) -> Ax.F(x))。
  • substitiute: 代入操作。t/x と表記され 1つの変数x を1つの項t に置き換える帰納的操作。帰納的に定義されるが (∀xφ)[t/x] = ∀xφ, (∀yφ)[t/x] = ∀y(φ[t/x]) となることには注意。代入は 完全に統語的な操作であるが 無制限に代入操作をすることは(統語論的ではなく)意味論的・証明論的に許されない (例: y/x を ∃y¬(x=y) に施すと ∃y¬(y=y) になる。統語的には正しい挙動であるが、意味論的・証明論的には許容できない)。代入はスコーレム化の際に使う。
  • isCollisionFree: 上述のように 代入結果が変にならないための判定関数。「変になる」とは完全に意味論的・証明論的な概念だが、その判定自身は統語的に定義可能である。isCollisionFree(formula, term, variableName) というシグネチャを持つ。「論理式φが代入t/xに対してcollision-freeな場合t/xを行うと・・・・」というような意味論的・証明論的な規則を作るために利用。

4. 意味論的操作

意味論的同値関係を用いて論理式を変形する操作をまとめる

  • toPNF: 冠頭標準形への変形。意味論的同値性を再帰的に適用することで、量化子が論理式の先頭にのみ存在するように変形できる。同値変形の際に衝突判定を回避する変数を選ぶ必要があるが、もっと緩く未使用の変数を採用すればよい。利用する同値性:
    • ¬∀xφ = ∃x¬φ
    • 未使用の変数y で ∀xφ⋀ψ = ∀y(φ[y/x]⋀ψ), ∀xφ⋀ψ = ∀y(φ[y/x]⋀ψ)
    • 未使用の変数y で ∀xφ⋁ψ = ∀y(φ[y/x]⋁ψ), ∃xφ⋁ψ = ∃y(φ[y/x]⋁ψ)
    • 未使用の変数y で ∀xφ→ψ = ∃y(φ[y/x]→ψ), ∃xφ→ψ = ∀y(φ[y/x]→ψ)
    • 未使用の変数y で φ→∀xψ = ∀y(φ→ψ[y/x]), φ→∃xψ = ∃y(φ→ψ[y/x])
  • toSNF: スコーレム標準形への変形。意味論的同値性ではなく充足的同値性による式変形により、∀論理式に変形する。内部的には一度 toPNF を呼び出して冠頭標準形にしたのち、 occurenceOf で未使用の定数や変数の一覧を取得して帰納的に変形する。∀式にしたら、それを節形式にする(つまりCNFにする)のだが、ここまでくれば命題論理の方法が使える。

5. 最短ルート

目的が「任意の論理式を節形式にする」のであれば、前述したいくつかの操作は不要で、最低限必要な以下の操作を実装すればOK。parse, occurenceOf, substitiute, toPNF, toSNF。補助的に createNode, isPrimeFormula, isOpenFormula。あと確認用に repr, rearrange。何が言いたいかというと、自由変数/束縛変数/非衝突判定/束縛変数のリネームといった数理論理学で必要な概念は節形式にする上では不要だということ。言語で使用していない記号の一覧が occurenceOf で得られるので、衝突回避な代入は簡単に求まるという事情による。

最後に

議論・コメント・要望・指摘なんでも歓迎です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「Front-end Developer Handbook 2019」について超ざっくり翻訳

英語ができないエンジニアのメモです、
もし翻訳に間違いなどありましたら、編集リクエストを投げてもらえると助かります。。。

そもそも「Front-end Developer Handbook」とは

概要

原文はこちら

何が書いてあるの?

フロントエンドエンジニアに関わる技術やトレンドを幅広く記載している。各技術について深くは解説していないが、全体が1冊の本にまとまっている。
PDFでダウンロードしたり、Webブラウザ版をオンラインで読むことも可能。

誰が書いたの?

「開眼! JavaScript ―言語仕様から学ぶJavaScriptの本質」(O'Reilly)や「jQuery Cookbook」(O'Reilly)、「JavaScript Enlightenment」(O'Reilly)などを執筆したCody Lindley氏。
参考:https://github.com/codylindley

0.1 — Recap of Front-end Development in 2018

1. React had several notable releases this past year that included, lifecycle methods, context API, suspense, and React hooks.

Reactは、昨年、lifecycle methods, context API, suspense, React hooksなど注目すべきリリースをいくつか発表した。

2. Microsoft buys Github. Yeah, that happened.

MicrosoftがGithubを買収。

3. Fonts created by CSS became a thing.

CSSによって作成されたフォントが人気になる。

4. What I used to call front-end driven apps, gets labeled "serverless". Unfortunately, this term is overloaded. However, the term JAMstack does seem to be resonating with developers.

私がフロントエンド駆動のアプリと呼んでいたものは、「サーバーレス」というラベルが付けられている。残念ながら、この用語は過負荷である。しかし、JAMstackという用語は開発者に共鳴している。

5. Google offered some neat tools this year to help make webpages load faster, i.e. squoosh and quicklink.

Googleは今年、ウェブページの読み込みを速くするための便利なツール(squooshやquicklink)を提案した。

6. Vue gets more Github stars than React this year. But React remains dominate in terms of use.

Vueは今年、Reactより多くのGithubスターを獲得した。しかし、Reactは依然として利用の面で支配している。

7. A solution similar to React, without a virtual DOM or JSX, is introduced RE:DOM.

仮想DOMやJSXを使用せずにReactに似たソリューションが、RE:DOMとして導入された。

8. Alternatives to NW.js and Electron show up, DeskGap and Neutralino.js.

NW.jsとElectronに代わる、DeskGapとNeutralino.jsが登場。

9. In 2017 the great divide between a front-end HTML & CSS developer v.s. front-end application developer is realized/verbalized. In 2018 that divide has grown wider and deeper and more people start to feel the divide.

2017年、front-end HTML & CSS developer と front-end application developer の間にある大きな格差がはっきり/言語化された。 2018年、その格差はより広く、より深くなり、より多くの人々がその格差を感じ始めた。

.10 This year, like most recent years, was stock full of app/framework solutions trying to contend with the mainstream JavaScript app tools (i.e. React, Angular, and Vue etc...) Let me list them for you. Radi.js, DisplayJS, Stimulus, Omi, Quasar.

ここ数年のように、今年も、主流のJavaScriptアプリツール(React、Angular、Vueなど)と対抗しようとするアプリ/フレームワークソリューションが溢れていた。リストアップしよう。 Radi.js, DisplayJS, Stimulus, Omi, Quasar。

11. JavaScript frameworks start offering their own languages that compile to JavaScript (e.g. Mint).

JavaScriptフレームワークは、JavaScriptにコンパイルされる独自の言語(例えばMint)の提供を開始する。

12. CodeSandbox evolves to become the dominant solution for online code sharing.

CodeSandboxは、オンラインコード共有のための主要なソリューションになるまで進化している。

13. CSS Grid and CSS Flexbox are fully supported in modern browsers and get taken for some serious rides. But many are left wondering when to use which one and how.

CSS GridとCSS Flexboxは最近のブラウザで完全にサポートされており、いくつかの深刻な問題に対応している。しかし、それらをどのように使用するのか、多くの人が疑問に思ったままである。

14. Many realize the long terms costs of bolted on type systems (e.g. TypeScript and Flow). Some concluded bolted on systems are not unlike bolted on module systems (i.e. AMD/Require.js) and come with more issues than solutions. Minimally, many developers realize that if types are needed in large code bases, that bolted on systems are not ideal in comparison to languages that have them baked in (e.g. Reason, Purescript, Elm).

多くの人が、型(TypeScriptやFlowなど)を導入することによる長期的なコストを認識している。
一部では、システムに固定されるのと、モジュールに固定される(AMD/Require.js)では違わず、解決よりも多くの問題があると結論づけた。
多くの開発者は、大規模なコードベースで型が必要な場合、システムで固定されていることが、それらを焼き付けた言語(Reason、Purescript、Elmなど)と比べて理想的ではないことを認識している。

15. CSS Variables gain browser support among modern web browsers

CSS Variablesは最近のウェブブラウザの間でブラウザサポートを得る

16. The flavors of CSS in JS exploded and some question the practice.

CSS in JSの人気は爆発的であり、そのプラクティスに疑問を抱く人もいる

17. ES modules are now usable in modern browsers and dynamic imports are close behind. We are even seeing a shift in tooling around this fact.

ESモジュールは現在、最新のブラウザで使用でき、動的インポートはすぐ後ろにある。

18. Many realize that end to end testing is the starting point of doing tests correctly in large part due to Cypress (i.e. Cypress first, then Jest).

多くの人が、E2EテストがCypressによるテストの大部分を正しく行うための出発点であることを認識している(Cypressが最初、次にJest)。

19. While Webpack was heavily used again this year, many developers found Parcel to be easier to get up and running.

Webpackは今年も頻繁に使われるようになったが、多くの開発者はParcelの方が起動しやすく実行しやすいと感じた。

20. One of the most important questions asked this year was, what is the cost of JavaScript.

今年の最も重要な論点の1つは、JavaScriptのコストである。

21. Babel 7 was released this year. That's a big deal because the last major release was almost three years ago.

Babel7は今年リリースされた。最後のメジャーリリースはほぼ3年前だったので、それは大事件である。

21. The reality of too much JavaScript change too fast is realized and people start talking about what you need to know before you can even learn something like React. The fight is real.

JavaScriptの変更が早すぎるという真実が現実のものとなり、Reactのようなことを学ぶ前に、知っておくべきことについて人々が話し始めている。戦いは本物だ。

22. Most developers found GraphQL, via Apollo, and see it as the next evolution for data API's.

ほとんどの開発者はApolloを介してGraphQLを見つけ、データAPIの次の進化と見なしている。

23. Gulp and friends definitely took a back seat to NPM/Yarn run. But this did not stop Microsoft from getting in the game with Just.

Gulpと友達は間違いなくNPM / Yarn runに後部座席を取った。しかし、これはMicrosoftがJustに参戦するのを妨げるものではなかった。

24. This year, one can not only lint/hint HTML, CSS, and JavaScript they can lint/hint the web itself.

今年は、HTML、CSS、およびJavaScriptだけでなく、Web自体をlint/hintにすることができる

25. The 2018 Front-End Tooling survey is worth reading if only to realize just how much jQuery is still used.

2018年のFront-End Tooling調査は、どれだけのjQueryがまだ使用されているかを理解するためだけに読む価値がある。

26. It can't be denied TypeScript gained a lot of users this year.

TypeScriptが今年多くのユーザーを獲得したことは否定できない。

27. VScode, dominates as the code editor of choice.

VScodeは、選択のコードエディタとして優位。

0.2 — In 2019, Expect...

1. Hopefully, more of this to come. "Stepping away from Sass".

うまくいけば、もっとくるだろう。 「Sassから離れる」

2. Still a good idea to keep an eye on and learn about the up coming additions (and potential additions) to CSS via https://cssdb.org

https://cssdb.org を通じて、CSSに今後追加される機能(および将来追加される可能性のある機能)について注意を払い、学習することをお勧めする。

The WebP image format from Google could reach support from all modern browsers this year.

3. GoogleからのWebP画像フォーマットは、今年、すべてのモダンブラウザからの支持を得ることができた。

4. Prepack will continue to cook.

Prepackは調理を続けるでしょう。

5. GraphQL will continue to gain massive adoption.

GraphQLは大規模的に採用され続けるだろう。

4. The, "State of JavaScript" survey authors will add a "State of CSS" survey in 2019.

"State of JavaScript"調査の著者は、2019年に "State of CSS"調査を追加する予定だろう。

5. Keep an eye on Web Animations API.

Web Animations APIに注目。

6. Someone you know will try and convince you to use TypeScript.

あなたの知っている誰かが、TypeScriptを試して、あなたに使うように説得するだろう。

7. Babel will get some competition from swc-project.

Babelはswc-projectとの競争をある程度受けるでしょう。

8. The case for, JAMStack's will continue.

JAMStackのは続くだろう。

9. Chasing the one code base to many platforms will continue.

1つのコードベースを多くのプラットフォームに追いかけるのは続くだろう。

10. More developers will turn to languages like ReasonML over JavaScript/TypeScript for large code bases.

より多くの開発者は、大きなコードベースのためにJavaScript/TypeScriptよりもReasonMLのような言語に目を向けるだろう。

11. More, largely used projects will start to shed jQuery in favor of native DOM solutions.

さらに、主に使用されているプロジェクトはネイティブDOMソリューションを支持してjQueryを脱ぎ捨て始めるだろう。

12. Web Components! At this point, I have no idea how Web Components will play out. Reality is they are not going away, and they have not gained a lot of momentum/usage once the hype ended.

Webコンポーネント!現時点では、Webコンポーネントがどのように機能するかはわからない。実際に、それらは消え去っていないし、誇大宣伝が終わっても多くの勢い/用法を得ていない。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

分割代入とスプレッドをまとめてみる

はじめに

最近のJavaScriptで追加された

  • 分割代入構文(Destructuring assignment syntax)
  • スプレッド構文(Spread syntax)
  • レスト構文(Rest parameter syntax)
  • 略記法

などがしっかりと区別がついていませんでした。
調べながらだと時間がとられるので一度簡単にまとめて覚えなおしてみたいと思いました。
最近の追加等を完全に追えていないので色々抜けがあるかもしれません。

Iterable object

大分ざっくりと言うならば、配列です。
その他、配列のように操作できfor of文などでループさせられるオブジェクトです。
これには深入りしません。

レスト構文(Rest parameter syntax)

レスト構文は、可変長の仮引数に1つの配列でアクセス出来るようにする構文です。
仮引数の最後に記述します。

function f(a, b, ...args) {
  console.log(a, b, args);
}

f(1, 2); // 1, 2, []
f(1, 2, 3, 4, 5); // 1, 2, [3, 4, 5]

使い道が分かりませんが、rest parameterを使うと個々の仮引数に分割代入出来るようになります。

function f1(a, b, ...[c, d, e]) {
  console.log(a, b, c, d, e);
}

f1(1, 2, 3, 4, 5, 6, 7) // 1, 2, 3, 4, 5

スプレッド構文(Spread syntax)

スプレッド構文は、Iterable objectを0個以上の値に展開する構文です。

関数呼び出しでのスプレッド

関数呼び出しで使うと、展開された値を関数に渡します。

function f2(a, b, c) {
  console.log(a, b, c);
}

let array = [1, 2, 3, 4, 5, 6];

f2(...array); // 1, 2, 3

Rest parameterとの組み合わせることも出来ます。
Rest parameterとは動作も違うので何回使っても良いです。

function f3(a, b, c, ...args) {
  console.log(a, b, c, ...args);
}

let array1 = [1, 2, 3, 4];
let array2 = [5, 6, 7, 8];
let array3 = [9, 10, 11, 12];

f3(...array1); // 1, 2, 3, [4]
f3(...array1, ...array2, ...array3); // 1, 2, 3, [4, 5, 6, 7, 8, 9, 10, 11, 12]

配列リテラルでのスプレッド

配列リテラルで使うと、展開された値が配列の要素として格納されます。

let array1 = [1, 2, 3];
let array2 = array1;
let array3 = [...array1];

console.log(array1, array2, array3); // [1, 2, 3] [1, 2, 3] [1, 2, 3]

array1.push("a");
array2.push("b");
array3.push("c");

console.log(array1, array2, array3);// [1, 2, 3, "a", "b"] [1, 2, 3, "a", "b"] [1, 2, 3, "c"]  

オブジェクトリテラルでのスプレッド

オブジェクトリテラルで使うと、名前と値の組に展開されてオブジェクトの要素として格納されます。

let array1 = [1, 2, 3];
let array2 = [3, 4, 5];
let obj1 = {a:1, b:2, c:3};
let obj2 = {c:4, d:5, e:6};

let result1 = {...array1};
let result2 = {...array1, ...array2};
let result3 = {...obj1};
let result4 = {...obj1, ...obj2};

console.log(result1); // {0: 1, 1: 2, 2: 3}
console.log(result2); // {0: 3, 1: 4, 2: 5}
console.log(result3); // {a: 1, b: 2, c: 3}
console.log(result4); // {a: 1, b: 2, c: 4, d: 5, e:6}

分割代入構文(Destructuring assignment syntax)

分割代入構文は、配列またはオブジェクトから値を取り出して別個の変数に代入する構文です。

配列への分割代入

let [a, b] = [1, 2];
let c, d;
[c, d] = [3, 4]

console.log(a, b, c, d) // 1 2 3 4

必要な値だけを取ることも出来ます。

let [a, b, , , e, f] = [1, 2, 3, 4, 5, 6];

console.log(a, b, e, f) // 1 2 5 6

レスト構文と組み合わせることも可能です。

let [a, b, ...rest] = [1, 2, 3, 4];

console.log(a, b, rest) // 1 2 [3, 4]

デフォルト値も設定できます。
値が代入されなかった場合に使用されます。

let [a=-1, b=-1, c=-1, d=-1] = [1, 2];

console.log(a, b, c, d) // 1 2 -1 -1

オブジェクトへの分割代入

代入するオブジェクトのキー名と変数名を一致させないと代入されません。
また、定義済みの変数に分割代入する場合は、以下のように()で囲む必要があります。

let { a, b } = { a: 1, b: 2 };
let c, d;
({ c, d } = { c: 3, d: 4 })
let { e, f, ...rest } = { e: 5, f: 6, g: 7, h: 8 };

let { g=-1, h=-1, i=-1, j=-1 } = { g: 7, h: 8 };

console.log(a, b) // 1 2
console.log(c, d) // 3 4
console.log(e, f, rest) // 5 6 {g: 7, h: 8}
console.log(g, h, i, j) // 7 8 -1 -1

オブジェクトのキー名と異なる名前の変数に代入したい場合は、以下のようになります。
慣れるまで戸惑いそうな記述と思いますがどうでしょうか。

let { a: foo, b: bar } = { a: 1, b: 2 } // fooを作成しaの値1を代入。barを作成しbの値2を代入

console.log(foo, bar); // 1 2

Shorthand

関連する構文として、オブジェクト作成時の略記法もあります。

コードに余計なアンダースコアが入っています。
Qiitaのバグのようで、何らかの文字を付けないと表示できないです。
リンクとして処理されてしまっているようです。

let x = "xx";
let y = "yy";
let z = "zz";
let obj = { x, y, z };
console.log(obj); //{x: "xx" y: "yy" z: "zz"};

let abc = "efg";
let obj = {
_  [abc]: 4,
_  [abc + "hij"]: 5
};
console.log(obj); // {efg: 4, efghij: 5}

応用

関数の戻り値

当たり前の話ですが、分割代入およびスプレッドは、関数の戻り値が配列およびオブジェクトの場合でも出来ます。

関数の戻り値 配列の場合

function f5() {
  return [1, 2];
}

let [a, b] = f5();
console.log(a, b); // 1 2
let c = [...f5()];
let d = f5();
console.log(c, d); // [1, 2] [1, 2]

関数の戻り値 オブジェクトの場合

function f6() {
  return { a:1, b:2 };
}

let {a, b} = f6();
console.log(a, b); // 1 2
let c = {...f6()};
let d = f6();
console.log(c, d); // { a:1, b:2 } { a:1, b:2 }

関数の引数

分割代入は関数の引数にも使用できます。

関数の引数 配列の場合

f1の方が、融通が利きそうですがどうでしょうか。

function f1([a, b, c]) { // 分割代入
  console.log(`${a} ${b} ${c}`);
}

function f2(a, b, c) {
  console.log(`${a} ${b} ${c}`);
}

let arr = [1, 2, 3];
f1(arr); // 1 2 3
//f1([...arr]); // 1 2 3 スプレッドする必要はない
f2(...arr); // 1 2 3

let [a, b, c] = [1 ,2, 3];
f1([a, b, c]); // 1 2 3
f2(a, b, c); // 1 2 3

関数の引数 オブジェクトの場合

デフォルト値を個別に設定できるf2が一番良さそうです。

function f1(obj) { // 代入
  console.log(`${obj.a} ${obj.b} ${obj.c}`);
}

function f2({a, b, c}) { // 分割代入
  console.log(`${a} ${b} ${c}`);
}

function f3(a, b, c) {
  console.log(`${a} ${b} ${c}`);
}

let obj = { a: 1, b: 2, c: 3 };
f1(obj); // 1 2 3
f2(obj); // 1 2 3
// f2({...obj}); // 1 2 3 スプレッドする必要はない
f3(...Object.values(obj));  // 1 2 3

let [a, b, c] = [1 ,2, 3];
f1({a, b, c}); // 1 2 3
f2({a, b, c}); // 1 2 3
f3(a, b, c); // 1 2 3

let obj2 = {x:1, y:2, z:3};
f1({a: obj2.x, b: obj2.y, c: obj2.z}); // 1 2 3
f2({a: obj2.x, b: obj2.y, c: obj2.z}); // 1 2 3
f3(...Object.values(obj2)); // 1 2 3

最後に

分割代入とスプレッドはまとめて覚えたほうが良さそうですね。
関数とのやり取りに使って真価を発揮する感じでしょうか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firebase Authenticationで作成されたユーザーをFirestoreに放り込んでいろいろしたい

やりたいこと

Firebase Authenticationでログインしたユーザーに、より多くの情報を持たせたい。
ので Firestore で 'users' collection を作ってFirebase Authenticationのユーザーをそこに放り込んでいろいろしたい。

とりあえずコード

function googleLogin() {
  const googleAuth = new firebase.auth.GoogleAuthProvider();
  firebase.auth().signInWithPopup(googleAuth).then((cred) => {
    const db = firebase.firestore();
    const users = db.collection('users');
    users.doc(cred.user.uid).set({
      datetime: moment().format('YYYY/MM/DD HH:mm');
    });
  });
}

流れ

  1. Firebase Authenticationでログイン(今回はGoogle Auth)
  2. [1]でログインしたユーザーに自動的に固有のIDが付与される
  3. Firestore に 'users' collection を作る
  4. [3]に[2]のユーザーIDと同じIDのdocumentを作る

詳細

Googleアカウントでログイン

const googleAuth = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(googleAuth);

ここまででログインはできます。

ログインしたユーザーの情報を得る

firebase.auth().signInWithPopup(googleAuth).then((cred) => {
  ...
})

signInWithRedirect という方法もあるけど、それだとユーザー情報を返してくれません。
signInWithPopupだとcredentialreturnしてくれるのでそれを使います。

users collectionを作成する

const db = firebase.firestore();
const users = db.collection('users');

users collectionの中にdocumentを作成する

users.doc(cred.user.uid).set({
  datetime: moment().format('YYYY/MM/DD HH:mm');
})

documentがひとつも入ってないとcollectionを作ってくれません。
なのでとりあえず作成日時でも入れておきます。
(ここではmoment.jsを使用しています)

おしまい

これでuser IDをdocumentで管理して都度突合したりしなくていい!やったー!

参考

Firebase Auth Tutorial #15- Firestore Users Collection

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JS】完全理解!?値渡しと参照渡しの使い分け!?

はじめに

値渡し参照渡し

この2つの言葉、プログラミングを勉強したことがある方なら聴いた事があるんじゃないでしょうか?

今回はJavaScriptにおける値渡しと参照渡しの使い分け方を調査してみました!

そのほかにも、JavaScriptの年収新恋人についての情報も!?

記法について

文字アレルギーの方でも読みやすいように、今回は2ちゃんねる風ワイ記法を用いて解説していこうと思います!

ぶいはち君とワイ

ワイ「ぶいはち君、aっていう変数を作ってくれや。aの中身は10や」

ぶいはち君「a = 10やな。覚えとくで」

ワイ「次はbを作ってくれや。中身はaと同じや」

ぶいはち君「aと同じってことは、10やな」
ぶいはち君「つまりb = 10、覚えておくで」

ワイ「すまん、事情が変わったから、aの値は500に変えてくれや」

ぶいはち君「a = 500やな。分かったで」

ワイ「ちなみにbの値は何やったっけ?」

ぶいはち君「b?」
ぶいはち君「b10やで」

値渡しについて

JavaScriptでは、数値や文字列は値渡しされます。
値渡しの場合は「aの値を500に変えてしまっても、bの値は元のまま10のまま」という挙動になります。

bの値はaと同じやで」と言われても「うんうん、aと同じやな」と記憶するのではなくb = 10やな」と置き換えて考えてくれるのです。
ちゃんと値自体をコピーしてくれる、ということですね。

逆に、参照渡しは

「参照渡し」の場合は「bは、aと同じやな!」と記憶するイメージです。
「bの値を聞かれたら、aの中身を見て答えればええわ!」って感じです。
(少し語弊があるのですが、とりあえず読み進んでみましょう)

再び、ぶいはち君とワイ

ワイ「ぶいはち君、aっちゅう配列を作ってくれや」
ワイ「中身は1, 2, 3, 4や」

ぶいはち君「あいよ、覚えとくで」

ワイ「bっていう配列も作ってくれや」
ワイ「中身はaと同じや」

ぶいはち君「配列bは、配列aと同じ
ぶいはち君「覚えたで」

ワイ「事情が変わったわ」
ワイ「配列aの0番目の値4に変えてくれ」

ぶいはち君「あいよ、ほな配列aの中身は4, 2, 3, 4やな」

ワイ「ところで、配列b0番目の値は何やったっけ」

ぶいはち君「配列b?
ぶいはち君「ああ、配列aと同じやつやな」
ぶいはち君「ほな、配列aを見て答えればええな」
ぶいはち君「ええと、配列b0番目の値は4や!」

参照渡しについて

JavaScriptでは、配列とオブジェクトは参照渡し1されます。

配列bの値を聞かれたら、配列aの中身を参照や!」みたいなイメージです。

なので、配列aの中身をいじった後で配列bの内容を確認すると「bの内容も変わっとるやんけ!」となります。

厳密には参照先のメモリアドレスを記憶するらしいので、以下のようにイメージすると良いかもしれません。

みたび、ぶいはち君とワイ

ワイ「bという配列を作ってくれや。中身はaと同じや」

ぶいはち君「あいよ」

ぶいはち君「aという配列はどこにしまったっけな」
ぶいはち君「メモによると、3番目の引き出しやな」

ぶいはち君「bいう配列もaと同じやって言うてたな」
ぶいはち君「メモっとかんとな」

ぶいはち君「bって言われたら3番目の引き出しを参照、と」

値渡しと参照渡しの使い分け方は!?

JavaScriptでは自動的に「数値や文字列は値渡し」「配列やオブジェクトは参照渡し」となるので、使い分けはできないということが分かりました!

「この変数の中身を変えると、他の変数の中身も変わってまうんか?」
ということだけ意識しておけば良いと思います!

ちなみに

const array = [1, 2, 3, 4];
const arrayCopy = [].concat(array);

↑こんな感じでconcatメソッド等を使用することで、配列を値渡しっぽく丸ごとコピーできたりしますが、
シャローコピー(1段階の深さのコピー)にしかならないので、多重配列の場合は一部が参照渡しとなってしまい、正しくコピーされません。

↓こんな風に、一回JSON文字列に変換して、再パースすればええやん!などという強者もいるようです(いない)。

const json = JSON.stringify(deepNestedObject);
const objectCopy = JSON.parse(json);

「コピーガードされとるDVDでも、再生しながらビデオカメラで録画すれば複製できるで!」
みたいな感じでつよいですね!(つよい)

JavaScriptの年収は!?

JavaScriptは各種Webブラウザ上で動作するプログラミング言語ですが、なんと無料で使用することができます。
つまり、JavaScriptの年収は0円だと分かりました!

JavaScriptプログラマーの年収という意味で言うと、概ね50万円〜5,000兆円の間に収まると考えられます。

JavaScriptの新恋人は!?

JavaScriptはプログラミング言語なので、恋愛はしないと考えられます

強いて言えば、JavaScriptの恋人はJavaScriptプログラマーある皆さんであると言えるのではないでしょうか!?

おわりに

いかがでしたか?

残念ながら今回、値渡しと参照渡しの使い分け方は見つけられませんでした
しかし、JavaScriptの恋人は皆さんだということが分かりました!

それでは、素晴らしいJavaライフを!

〜Fin〜

ここから追記:

コメント欄でご指摘いただいた内容について書くで!

ワイ「bという配列を作ってくれや。中身はaと同じや」

ぶいはち君「あいよ」
ぶいはち君「bて言われたらaを参照やな」
ぶいはち君「完全に理解したで」

ワイ「やっぱり、aの中身は配列やなくて」
ワイ「ただの3という数値をぶち込んどいてくれや!」

ぶいはち君「あいよ」

ワイ「ちなみにbの中身はなんやったっけ?」

ぶいはち君「さっきaの中身は変わったけど・・・」
ぶいはち君「b配列のままや!(キリッ」

ワイ「あれ?」
ワイ「bの値を答えるときはaを参照するんやなかったっけ・・・」

参照渡しっぽい何かだった

配列やオブジェクトの一部を編集した場合はaにもbにもその変更が反映されますが、
再代入した場合は連動しないのです!

いかがでしたか!?


  1. 実際には、C言語等の参照渡しとは違う「参照渡しっぽい値渡し」です。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JS】完全に理解した!?値渡しと参照渡しの使い分け

はじめに

値渡し参照渡し

この2つの言葉、プログラミングを勉強したことがある方なら聴いた事があるんじゃないでしょうか?

今回はJavaScriptにおける値渡しと参照渡しの使い分け方を調査してみました!

そのほかにも、JavaScriptの年収新恋人についての情報も!?

記法について

文字アレルギーの方でも読みやすいように、今回は2ちゃんねる風ワイ記法を用いて解説していこうと思います!

ぶいはち君とワイ

ワイ「ぶいはち君、aっていう変数を作ってくれや。aの中身は10や」

ぶいはち君「a = 10やな。覚えとくで」

ワイ「次はbを作ってくれや。中身はaと同じや」

ぶいはち君「aと同じってことは、10やな」
ぶいはち君「つまりb = 10、覚えておくで」

ワイ「すまん、事情が変わったから、aの値は500に変えてくれや」

ぶいはち君「a = 500やな。分かったで」

ワイ「ちなみにbの値は何やったっけ?」

ぶいはち君「b?」
ぶいはち君「b10やで」

値渡しについて

JavaScriptでは、数値や文字列は値渡しされます。
値渡しの場合は「aの値を500に変えてしまっても、bの値は元のまま10のまま」という挙動になります。

bの値はaと同じやで」と言われても「うんうん、aと同じやな」と記憶するのではなくb = 10やな」と置き換えて考えてくれるのです。
ちゃんと値自体をコピーしてくれる、ということですね。

逆に、参照渡しは

「参照渡し」の場合は「bは、aと同じやな!」と記憶するイメージです。
「bの値を聞かれたら、aの中身を見て答えればええわ!」って感じです。
(少し語弊があるのですが、とりあえず読み進んでみましょう)

再び、ぶいはち君とワイ

ワイ「ぶいはち君、aっちゅう配列を作ってくれや」
ワイ「中身は1, 2, 3, 4や」

ぶいはち君「あいよ、覚えとくで」

ワイ「bっていう配列も作ってくれや」
ワイ「中身はaと同じや」

ぶいはち君「配列bは、配列aと同じ
ぶいはち君「覚えたで」

ワイ「事情が変わったわ」
ワイ「配列aの0番目の値4に変えてくれ」

ぶいはち君「あいよ、ほな配列aの中身は4, 2, 3, 4やな」

ワイ「ところで、配列b0番目の値は何やったっけ」

ぶいはち君「配列b?
ぶいはち君「ああ、配列aと同じやつやな」
ぶいはち君「ほな、配列aを見て答えればええな」
ぶいはち君「ええと、配列b0番目の値は4や!」

参照渡しについて

JavaScriptでは、配列とオブジェクトは参照渡し1されます。

配列bの値を聞かれたら、配列aの中身を参照や!」みたいなイメージです。

なので、配列aの中身をいじった後で配列bの内容を確認すると「bの内容も変わっとるやんけ!」となります。

厳密には参照先のメモリアドレスを記憶するらしいので、以下のようにイメージすると良いかもしれません。

みたび、ぶいはち君とワイ

ワイ「bという配列を作ってくれや。中身はaと同じや」

ぶいはち君「あいよ」

ぶいはち君「aという配列はどこにしまったっけな」
ぶいはち君「メモによると、3番目の引き出しやな」

ぶいはち君「bいう配列もaと同じやって言うてたな」
ぶいはち君「メモっとかんとな」

ぶいはち君「bって言われたら3番目の引き出しを参照、と」

値渡しと参照渡しの使い分け方は!?

JavaScriptでは自動的に「数値や文字列は値渡し」「配列やオブジェクトは参照渡し」となるので、使い分けはしなくてよいということが分かりました!

「この変数の中身を変えると、他の変数の中身も変わってまうんか?」
ということだけ意識しておけば良いと思います!

ちなみに

const array = [1, 2, 3, 4];
const arrayCopy = [].concat(array);

↑こんな感じでconcatメソッド等を使用することで、配列を値渡しっぽく丸ごとコピーできたりしますが、
シャローコピー(1段階の深さのコピー)にしかならないので、多重配列の場合は一部が参照渡しとなってしまい、正しくコピーされません。

↓こんな風に、一回JSON文字列に変換して、再パースすればええやん!などという強者もいるようです(いない)。

const json = JSON.stringify(deepNestedObject);
const objectCopy = JSON.parse(json);

「コピーガードされとるDVDでも、再生しながらビデオカメラで録画すれば複製できるで!」
みたいな感じでつよいですね!(つよい)

JavaScriptの年収は!?

JavaScriptは各種Webブラウザ上で動作するプログラミング言語ですが、なんと無料で使用することができます。
つまり、JavaScriptの年収は0円だと分かりました!

JavaScriptプログラマーの年収という意味で言うと、概ね50万円〜5,000兆円の間に収まると考えられます。

JavaScriptの新恋人は!?

JavaScriptはプログラミング言語なので、恋愛はしないと考えられます

強いて言えば、JavaScriptの恋人はJavaScriptプログラマーある皆さんであると言えるのではないでしょうか!?

おわりに

いかがでしたか?

残念ながら今回、値渡しと参照渡しの使い分け方は見つけられませんでした
しかし、JavaScriptの恋人は皆さんだということが分かりました!

それでは、素晴らしいJavaライフを!

〜Fin〜

ここから追記:

コメント欄でご指摘いただいた内容について書くで!

ワイ「bという配列を作ってくれや。中身はaと同じや」

ぶいはち君「あいよ」
ぶいはち君「bて言われたらaを参照やな」
ぶいはち君「完全に理解したで」

ワイ「やっぱり、aの中身は配列やなくて」
ワイ「ただの3という数値をぶち込んどいてくれや!」

ぶいはち君「あいよ」

ワイ「ちなみにbの中身はなんやったっけ?」

ぶいはち君「さっきaの中身は変わったけど・・・」
ぶいはち君「b配列のままや!(キリッ」

ワイ「あれ?」
ワイ「bの値を答えるときはaを参照するんやなかったっけ・・・」

参照渡しっぽい何かだった

配列やオブジェクトの一部を編集した場合はaにもbにもその変更が反映されますが、
再代入した場合は連動しないのです!

いかがでしたか!?


  1. 実際には、C言語等の参照渡しとは違う「参照渡しっぽい値渡し」です。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flow で Flux データフロー実装に対し最小の型アノテーションで 100% の Type Coverage を得る方法

背景

Flux アーキテクチャは概念で、Redux はそれを薄く実装したライブラリだ。
実装を見るとコード量が少ないことに気が付く。

だからこそ Redux を使ったコードにはプログラマの癖が強く現れるし、コミュニティ上でプラクティスに関する議論が盛り上がるし、ドキュメントが長くもなる。

とはいえ、2019 年現在にもなれば、もうプラクティスは出尽くした感がある1

であれば、これをより堅牢に運用できるよう、うまく型をつける方法についても整理してみようと思う。

なお、非同期 Action の実装には redux-thunkredux-promiseredux-saga も使わず、Vanilla な async/await を使う。こうしたほうが、Flux アーキテクチャの原型がつかみやすく、型が付けるのが楽で、またこの記事にとって本質ではない Middleware の説明も端折れる。

ここで書いた実装や型付けの方式は、私が副業で開発に参加している Findy のプロダクト開発で実運用している。

Flux Standard Action への準拠

Action オブジェクトは、Redux コミュニティにおけるベストプラクティスの一つ Flux Standard Action (FSA) の型に準拠させる。

これに準拠する型 StandardActionT を独自に定義しておく。
型引数 T に Action Type 名、P に payload のデータ型を渡す形で利用する。

declare type StandardActionT<T, P> =
  | {|
      type: T,
      payload: P,
      error?: false,
      meta?: mixed
    |}
  | {|
      type: T,
      payload: Error,
      error: true,
      meta?: mixed
    |}

使用例はこうなる。

const deleteUser = (
  userId: number
): StandardActionT<'DELETE_USER', number> => ({
  type: 'DELETE_USER',   // 'DELETE_USER' 以外の文字列だとエラーになる
  payload: userId,       // payload の型が number 以外だとエラーになる
  // a: 1                // 左のように、 FSA で規定されていない property を持たせるとエラーになる
});

ここまでのサンプル

Action Creator に対する型付け

同期 Action Creator 編

同期 Action Creator 関数に対する型付けについては、上の deleteUser がそれそのものになっている。

非同期 Action Creator 編

async function は Promise を返すので、戻り値型の StandardActionTPromise 型で包む必要がある。

type UserT = {| id: number, name: string |}

const fetchUser = async (
  userId: number
): Promise<StandardActionT<'FETCH_USER', UserT>> => {
  const res = await axios.get(`/users/${userId}`).catch(e => e.response);
  return {
    type: 'FETCH_USER',
    payload: res.data.user  // 注意: ここは API レスポンスの中身であり、型アノテーションによってのみ型情報を持つことができる
  };
};

ここまでのサンプル

Reducer に対する型付け

いきなり最終的な結果を見るとアレルギーが出るかもしれないので、段階的に型アノテーションを付与していくようにして説明する。

State 型を付ける

まずは何よりも、Reducer の State の型が明示されていないと、Action Creator との協調や、Component とのつなぎ込みなどの全てが難しくなる。ここの型情報はなんとしても死守したい。

そこで StateT を定義し、引数および戻り値のそれぞれが StateT であることをアノテーションする。

type StateT = $ReadOnlyArray<UserT>;

const users = (state: StateT, action: any /* TODO */): StateT => {
  /* 省略 */
}

Reducer が observe する Action 型を定義する(同期編)

この Reducer が関与する Action 型を定義する。
これと State 型を両方与えることで、それぞれの型定義が協調し、実際に完全な型チェックが機能する。

上で例示した同期 Action Creator 関数の deleteUserを用いた例は次のようになる。

/* Entity Types */

type UserT = {| id: number, name: string |};

/* Action creators */

const deleteUser = (
  userId: number
): StandardActionT<'DELETE_USER', number> => ({
  type: 'DELETE_USER',
  payload: userId
});

/* Reducer */

type StateT = $ReadOnlyArray<UserT>;
type ActionT = $Call<typeof deleteUser, *>

const users = (state: StateT, action: ActionT): StateT => {
  switch (action.type) {
    case 'DELETE_USER': {
      if (action.error) {
        return state;
      } else {
        return state.filter(user => user.id !== action.payload);
      }
    }
    default: {
      return state;
    }
  }
};

Action 型定義には、先述した方法で型付けした Action Creator 関数の戻り値の型を、$Call Utility Type を用いて使用する。こうすることで、Action Creator に対する型定義が Single Source of Truth となり、重複する型定義を各所に個別定義する必要がなくせる。

この段階で、完全な型チェックが機能する状態になる。
Action Creator & Reducer 型チェック確認用サンプル(同期 Action のみ)

このサンプルの Reducer 内にあるコメントアウトを外したり、Action Creator の戻り値型を変更してみたりすると、エラーとなることが確認できる。

Reducer が observe する Action 型を定義する(非同期編)

Action Creator 関数が非同期になっても、上の例と同じように、$Call で Action 型を取り出したい。

しかし、async function の戻り値は Promise<T> 型で包まれている。この型パラメータ部分を取り出して、Reducer の受け入れ可能な Action 型として参照するためにはどうすればいいだろう。

これを行う Utility Type を独自に定義することができる。

declare type $UnwrapPromise<T> = $Call<<T>(Promise<T>) => T, T>;

Promise<T> から T を返す関数」の型定義に対して $Call を呼ぶことで、Promise に包まれている型を計算している(参考 issue)。

この型定義をきちんと理解できている必要はなく、とにかく Promise に包まれている型が取り出せていそうなことが $UnwrapPromise 動作確認サンプル で確認できたら、「ふむ、なるほど」などと適当に相槌を打っておこう。

これを用いることで Action Creator を await で呼び出した戻り値型は、以下のように表現できる

$UnwrapPromise<$Call<typeof fetchUser, *>>

最終的に、これを用いた非同期 Action Creator 関数の fetchUser を用いた例は次のようになる。

/* Entity Types */

type UserT = {| id: number, name: string |};

/* Action creators */

const deleteUser = (
  userId: number
): StandardActionT<'DELETE_USER', number> => ({
  type: 'DELETE_USER',
  payload: userId
});

const fetchUser = async (
  userId: number
): Promise<StandardActionT<'FETCH_USER', UserT>> => {
  const res = await axios.get(`/users/${userId}`).catch(e => e.response);
  return {
    type: 'FETCH_USER',
    payload: res.data.user
  };
};

/* Reducer */

type StateT = $ReadOnlyArray<UserT>;
type ActionT =
  | $Call<typeof deleteUser, *>
  | $UnwrapPromise<$Call<typeof fetchUser, *>>;

const users = (state: StateT, action: ActionT): StateT => {
  switch (action.type) {
    case 'DELETE_USER': {
      if (action.error) {
        return state;
      } else {
        return state.filter(user => user.id !== action.payload);
      }
    }
    case 'FETCH_USER': {
      if (action.error) {
        return state;
      } else {
        const user = action.payload;
        return state.some(user => user.id === user.id)
          ? state
          : [...state, user];
      }
    }
    default: {
      return state;
    }
  }
};

型パラメータだらけで複雑に見えるが、それぞれを段階的に分解していけば正しい表現になっていることが分かる。一度納得できたら、あとはイディオムとして慣れるのみ。

型チェックが機能する状態については、以下のサンプルで確認できる。
Action Creator & Reducer 型チェック確認用サンプル(非同期 Action Creator 込み)

上記のサンプルで、実際どこまで型がチェックできているかというと、例えば Reducer の各 case 節内に入った時点で action 変数が type refinement されているので FETCH_USER 節の action.payloadUserT 型か Error 型である、といったところまでチェックされていて、完全に型チェックがされている。

まとめ

Action Creator 関数に対してのみ明示的な型アノテーションを付与し、それを Signle Source of Truth とし戻り値型を計算して使用することで、Action Creator 関数と Reducer とそれらの協調する実装に対して、完全な型定義を付与できた。

初見でアレルギーが起きる懸念はあるものの、それぞれの段階を分解して正しいことを一度理解できれば、あとは慣れの問題になる。

ところで近頃は Flow と TypeScript の人気がかなり明確に偏ってきていて、そろそろ TypeScript やっておかないとヤバそうな気配がある。


  1. となったあたりで React Hooks のリリースによりさらなる変化が 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Apps Script (GAS)で動的HTMLページを作成する方法!

Google Apps Script (GAS) を使って動的なHTMLページを作成します。誰でもURLを開けばGoogle Apps Script (GAS) をつかったWebアプリケーションページが表示され、今回はURLを入れるとURLに指定したパラメータを付与する事ができます。

ただお馴染みですが、4流以下のスキルですから大したコードをかけてないのが正直なところです。笑
なので紹介しているのはこれからGASを覚えようとか、覚えたてです!とか、GASってなーに?って人向けの記事ばかりです!

サイト紹介

以下様々なGASやその他記事を紹介してます!
https://bzbot.work/

紹介記事
今回紹介している記事は以下です!
https://bzbot.work/2019/04/17/gas-html/

作成完了イメージ

以下のように作成をしていきます。
image.png
以下作成した簡単なWebアプリケーション
https://script.google.com/macros/s/AKfycbz1V5z1WRE5d-Py1IFpXkgmzx8S-VCg2eLahWsXIMIxE79oBms/exec

.htmlの作成

index.htmlファイルを作成します。
image.png

CSS関連のデザインはいじろうとしていたので、余計なclassとかありますが気にしないでください。笑

ビジボット
<!-- bootstrapを使う -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
<!DOCTYPE html>
<html lang="ja">
<head>
  <base target="_top">
</head>
<body>
  <div class="container">
    <h1 class="display-4">ビジボットURL作成画面</h1>
    <!-- formはgetメソッド -->
    <form method="get" action="https://script.google.com/macros/s/***************************************/exec">
      <!-- ラジオボタンフィールド -->
      <div class="radio-inline">
        <label><input type="radio" name="radio" id="radio1" value="line" checked="checked">LINE</label>
        <label><input type="radio" name="radio" id="radio2" value="facebook">Facebook</label>
        <label><input type="radio" name="radio" id="radio3" value="instagram">Instagram</label>
        <label><input type="radio" name="radio" id="radio3" value="twitter">Twitter</label>
        <label><input type="radio" name="radio" id="radio3" value="google">Google</label>
      </div>
      <!-- URLを入力するフィールド -->
      <div class="form-group">
        <input type="text" name="url" size="100" value="" placeholder="セットしたいURLを入れてください">
        <input class="btn-dark" type="submit" value="生成">
      </div>
      <div class="output_area">
        <div class="form-group"><input type="text" size="100" name="source" value="<?=url?>" placeholder="指定したパラメータURLが生成されます"></div>
      </div>
    </form>
  </div>
</body>
</html>h

GASコード

HTML側からGETで受け取った値を処理して、再度HTML側に返します。

GAS
//method=getで送信されたら実行する
function doGet(e){
  //indexファイルのオブジェクト
  var html = HtmlService.createTemplateFromFile('index');

  Logger.log(e);
  //getで送信された値を指定して取得する(index.htmlファイルのname="url"部分)
  var url = e.parameter.url;
  //getで送信された値を指定して取得する(index.htmlファイルのname="radio"部分)
  var params = e.parameter.radio;

  //paramsで取得した値でurlにreturnさせるurlの値を指定する
  if(params === 'line'){
    var url = url + '?utm_source=line';    
  }else if(params === 'facebook'){
    var url = url + '?utm_source=facebook';    
  }else if(params === 'twitter'){
    var url = url + '?utm_source=twitter';
  }else if(params === 'instagram'){
    var url = url + '?utm_source=instagram';
  }else if(params === 'google'){
    var url = url + '?utm_source=google';
  }else{
    var url = '';
  }

  //index.htmlにurlを返す
  html.url = url;
  return html.evaluate(); 
}

詳しい手順説明はビジボットのサイトに記載していますのでそちらをご覧ください。

改めてサイト紹介

以下様々なGASやその他記事を紹介してます!
https://bzbot.work/

紹介記事
今回紹介している記事は以下です!
https://bzbot.work/2019/04/17/gas-html/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Githubの芝を短いコードでAAにする

コード

for(r=w='';w--+7;r+='\n')$$('.day').map((v,i)=>i%7-~w?0:r+='口圏国因囚'[-~v.attributes[5].value[5]>>1]);r

実行

  • Chromeで任意のGithubアカウントのページを開き、Consoleで上記コードを実行するとAAが得られます
    • サンプルに使用したのは、芝で絵を描くツール(skazhy/github-decorator)のテストアカウント

スクリーンショット 2019-04-17 2.48.26.png

解説

最初はdata-count属性(contribute数)で判別していましたが、場合によって閾値が変わるようだったので没に。
fill属性(色)の5桁目が都合良くバラけていたのでそれを利用しています。

色(v.attributes[5].value) -~v.attributes[5].value[5]>>1
#ebedf0 0
#c6e48b 4
#7bc96f 3
#239a3b 2
#196127 1

終わりに

JSゴルフは不慣れなので、短縮できたら教えて頂けると嬉しいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

令和を判定するコードを�自分が書くならどうするか考えた

発端

眠れないなーと思いつつスマホでこちらの記事を読みながら、
令和へ対応せよ!元号のアルゴリズム - Qiita

年*10000 + 月*100 + 日

あー、こんなコードって大昔に(VBAとかで)書いたんだが見たんだかしたことあるなーと思ったので、
自分ならどう書くかなーと思ったので殴り書き。

年月日に乗算して計算するのはバッドプラクティス

…だと思っています。月*100ではないですし、私は違和感があります。

端的に言うと、
データベースの数値型のカラムに数値で日付が入っていたらどう思うか?ってことです。1

というわけで書いてみた

function ymdToGengo(y, m, d) {
  // TODO 引数チェック
  const ts = new Date(`${y}-${('0'+m).slice(-2)}-${('0'+d).slice(-2)}T00:00:00.000+0900`).getTime();
  if (ts >= new Date('2019-05-01T00:00:00.000+0900').getTime()) {
    return '令和';
  }
  if (ts >= new Date('1989-01-08T00:00:00.000+0900').getTime()) {
    return '平成';
  }
  if (ts >= new Date('1926-12-25T00:00:00.000+0900').getTime()) {
    return '昭和';
  }
  return '大正以前';
}

console.assert(ymdToGengo(0, 1, 1) === '大正以前');
console.assert(ymdToGengo(1926, 12, 25) === '昭和');
console.assert(ymdToGengo(1989, 1, 8) === '平成');
console.assert(ymdToGengo(2019, 5, 1) === '令和');
  • 元のお題では年月日の数字がそれぞれあるっていう話だったので、引数はYMDで
    • 直接Dateを受け取るようにすれば、別のタイムゾーンで入ってきた日付も正常に判定できるはず
  • 比較はDateで比較対象作ってからtimestampで比較
  • ガード節を使おうぜ

気付き

console.log(new Date('2019-05-01T00:00:00.000+0900').toLocaleDateString('ja-JP-u-ca-japanese', {era:'long'}));
// 平成31年5月1日
  • でも手元のnode(v8.11.2)だと取れなかった(CE 2019 5 1と出力された)3
  • Chromeもまだ令和未対応
  • こうやって考えるとPHPのDateTimeって使い勝手いいよな

結論

もしnpmに元号変換ライブラリがあったらそれ使う。4


  1. 本当にあった怖い話(ていうかよくあった、よね?) 

  2. 一応、西暦645年でもgetTime()できたから全和暦判定をDateで書くのも無理ではない…? 

  3. なにこれ?(調べてない) 

  4. このへん? https://github.com/yukik/koyomi 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

令和を判定するコードを自分が書くならどうするか考えた

発端

眠れないなーと思いつつスマホでこちらの記事を読みながら、
令和へ対応せよ!元号のアルゴリズム - Qiita

年*10000 + 月*100 + 日

あー、こんなコードって大昔に(VBAとかで)書いたんだか見たんだかしたことあるなーと思ったので、
自分ならどう書くかなーと思ったので殴り書き。

年月日に乗算して計算するのはバッドプラクティス

…だと思っています。月*100ではないですし、私は違和感があります。

端的に言うと、
データベースの数値型のカラムに数値で日付が入っていたらどう思うか?ってことです。1

というわけで書いてみた

function ymdToGengo(y, m, d) {
  // TODO 引数チェック
  const ts = new Date(`${y}-${('0'+m).slice(-2)}-${('0'+d).slice(-2)}T00:00:00.000+0900`).getTime();
  if (ts >= new Date('2019-05-01T00:00:00.000+0900').getTime()) {
    return '令和';
  }
  if (ts >= new Date('1989-01-08T00:00:00.000+0900').getTime()) {
    return '平成';
  }
  if (ts >= new Date('1926-12-25T00:00:00.000+0900').getTime()) {
    return '昭和';
  }
  return '大正以前';
}

console.assert(ymdToGengo(0, 1, 1) === '大正以前');
console.assert(ymdToGengo(1926, 12, 25) === '昭和');
console.assert(ymdToGengo(1989, 1, 8) === '平成');
console.assert(ymdToGengo(2019, 5, 1) === '令和');
  • 元のお題では年月日の数字がそれぞれあるっていう話だったので、引数はYMDで
    • 直接Dateを受け取るようにすれば、別のタイムゾーンで入ってきた日付も正常に判定できるはず
  • 比較はDateで比較対象作ってからtimestampで比較
  • ガード節を使おうぜ

気付き

console.log(new Date('2019-05-01T00:00:00.000+0900').toLocaleDateString('ja-JP-u-ca-japanese', {era:'long'}));
// 平成31年5月1日
  • でも手元のnode(v8.11.2)だと取れなかった(CE 2019 5 1と出力された)3
  • Chromeもまだ令和未対応
  • こうやって考えるとPHPのDateTimeって使い勝手いいよな

結論

もしnpmに元号変換ライブラリがあったらそれ使う。4


  1. 本当にあった怖い話(ていうかよくあった、よね?) 

  2. 一応、西暦645年でもgetTime()できたから全和暦判定をDateで書くのも無理ではない…? 

  3. なにこれ?(調べてない) 

  4. このへん? https://github.com/yukik/koyomi 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AlexaとGoogleHomeを同じコードで実現するフレームワーク「Jovo」を試してみた

Jovoとは

こんばんは。家に帰って少し暇だったので遊んでみました。JovoとはAlexaとGoogleHomeを一つのコードベースで構築できる初のフレームワークです。ちょうど最近ver2.2がリリースされてTypeScriptへのサポートが強化されたとのことだったので触ってみました。今回はサンプルとして簡単な計算をする音声アプリを作ってみます。

インストール

インストールは非常に簡単です。
yarnを利用している場合は下記のコマンドを叩いてjovo-cliをグローバルにインストールしましょう。

$ yarn global add jovo-cli
$ jovo -v
Jovo CLI Version: 2.2.3

プロジェクトの作成

先ほどインストールしたjovo-cliを使用してプロジェクトを作成します。
TypeScriptを使用する場合は、オプションで --language typescriptをつけてください。

$ jovo new <directory> --language typescript

これで前準備が整ったのでまずはmodelを作っていきます。
models配下にen-US.jsonというファイルがあるのでこのファイルをリネームしてja-JP.jsonにします。
そしてこのjsonファイルを使って音声モデルを組み立てます。
今回は簡単な計算をするスキルなので以下のように組み立てました。

ja-JP.json
{
    "invocation": "Sample Jovo app",
    "intents": [
        {
            "name": "PlusIntent",
            "phrases": [
                "{number}を足して",
                "プラス{number}",
                "{number}を足す"
            ],
            "inputs": [
                {
                    "name": "number",
                    "type": {
                        "alexa": "AMAZON.NUMBER",
                        "dialogflow": "@sys.number"
                    }
                }
            ]
        },
        {
            "name": "MinusIntent",
            "phrases": [
                "{number}を引いて",
                "マイナス{number}",
                "{number}を引く"
            ],
            "inputs": [
                {
                    "name": "number",
                    "type": {
                        "alexa": "AMAZON.NUMBER",
                        "dialogflow": "@sys.number"
                    }
                }
            ]
        },

        {
            "name": "ResultAnswerIntent",
            "phrases": [
                "答えは{answer}",
                "{answer}"
            ],
            "inputs": [
                {
                    "name": "answer",
                    "type": {
                        "alexa": "AMAZON.NUMBER",
                        "dialogflow": "@sys.number"
                    }
                }
            ]
        },

        {
            "name": "ResultHelpIntent",
            "phrases": [
                "結果を教えて",
                "答えを教えて"
            ]
        }
    ],
    "alexa": {
        "interactionModel": {
            "languageModel": {
                "intents": [
                    {
                        "name": "AMAZON.CancelIntent",
                        "samples": []
                    },
                    {
                        "name": "AMAZON.HelpIntent",
                        "samples": []
                    },
                    {
                        "name": "AMAZON.StopIntent",
                        "samples": []
                    }
                ]
            }
        }
    },
    "dialogflow": {
        "intents": [
            {
                "name": "Default Fallback Intent",
                "auto": true,
                "webhookUsed": true,
                "fallbackIntent": true
            },
            {
                "name": "Default Welcome Intent",
                "auto": true,
                "webhookUsed": true,
                "events": [
                    {
                        "name": "WELCOME"
                    }
                ]
            }
        ]
    }
    }   

上記を見ればわかると思いますが、Intent内で変数を利用する場合はAlexa,Dialogflowそれぞれのプラットフォームのスロットやエンティティを定義してあげる必要があります。今回は使っていませんがカスタムのスロットやエンティティを利用する場合は下記のように共有で定義してあげればいいようです。もちろんsynonyms(類義語)などにも対応しているようです。


"inputs": [
    {
        "name": "city",
        // Use your own input type
        "type": "myCityInputType"
    }
]
"inputTypes": [
    {
        "name": "myCityInputType",
        "values": [
            {
                "value": "Berlin"
            },
            {
                "value": "New York",
                "synonyms": [
                    "New York City"
                ]
            }
        ]
    }
]

公式ドキュメントを参考に以下のサンプルを作ってみました。

app.ts
import {App} from 'jovo-framework';
import {Alexa} from 'jovo-platform-alexa';
import {JovoDebugger} from 'jovo-plugin-debugger';
import {FileDb} from 'jovo-db-filedb';
import {GoogleAssistant} from 'jovo-platform-googleassistant';

// ------------------------------------------------------------------
// APP INITIALIZATION
// ------------------------------------------------------------------

const app = new App();

app.use(
    new Alexa(),
    new GoogleAssistant(),
    new JovoDebugger(),
    new FileDb(),
);


// ------------------------------------------------------------------
// APP LOGIC
// ------------------------------------------------------------------

app.setHandler({
    LAUNCH() {
        return this.toIntent('WelcomeIntent');
    },

    WelcomeIntent() {
        this.ask("簡単な計算スキルです。足し算と引き算ができます。最初は0から始まります。一を足してや三を引いてなどを私に言ってください。");
    },

    PlusIntent() {
        const plusnumber : number = +this.$inputs.number.value;
        // session_attributesから値を引っ張ってくる
        const currentResult : number = +this.$session.$data.result || 0;
        const result : number = currentResult + plusnumber;
        // session_attributesにデータを格納する
        this.$session.$data.result = result;
        this.ask(plusnumber.toString() + "を足しました。計算をやめる場合は答えを言うか、答えを教えてなどと聞いてください");
    },
    MinusIntent() {
        const minusnumber : number = +this.$inputs.number.value;
        // session_attributesから値を引っ張ってくる
        const currentResult : number = +this.$session.$data.result || 0;
        const result : number = currentResult - minusnumber;
        // session_attributesにデータを格納する
        this.$session.$data.result = result;
        this.ask(minusnumber.toString() + "を引きました。計算をやめる場合は答えを言うか、答えを教えてなどと聞いてください");
    },
    ResultAnswerIntent() {
        const answer : number = +this.$inputs.answer.value;
        const currentResult : number = +this.$session.$data.result || 0;
        console.log(answer, currentResult);
        if (answer === currentResult){
            this.tell("正解です。答えは" + currentResult.toString() + "でした。");
        }
        else{
            this.ask("違います。わからなければ答えを教えてと聞いてみてください。");
        }
    },
    ResultHelpIntent(){
        const currentResult : number = +this.$session.$data.result || 0;
        this.tell("答えは" + currentResult.toString() + "でした。");
    }
});

export {app};

テスト

サンプルコードができたので以下のコマンドでローカルでコンパイルして起動をしてみます。

$ npm run tsc
$ jovo run

起動後に「.」を押すとブラウザ上でデバッガーを起動できます。
jovo_debug1.mov.gif

このデバッガーがかなり便利で、直感的にAlexaとGoogle Assistantの両方でマニュアルテストをすることができます。

$ npm run tscw$ jovo run --watchで実行すれば、ホットリロードをしてくれるのでスピード感を持った開発ができそうですね。
しかし、あくまで、このツールできるのは「文字でのテスト」になるので、実際に喋った時と同じ挙動をするかはまた別の話です。音声アプリを作る際には実機での検証が大切です。

また、ユニットテストをする際にはjestを利用するといいと思います。下記は起動リクエストを飛ばした時に正常なレスポンスが返ってくるかのテストです。

sample_test.ts
import {Alexa} from 'jovo-platform-alexa';
import {GoogleAssistant} from 'jovo-platform-googleassistant';

jest.setTimeout(500);

const launchMeaage = "簡単な計算スキルです。足し算と引き算ができます。最初は0から始まります。一を足してや三を引いてなどを私に言ってください。";

for (const p of [new Alexa(), new GoogleAssistant()]) {
    const testSuite = p.makeTestSuite();

    describe(`PLATFORM: ${p.constructor.name} INTENTS` , () => {
        test('should return a welcome message and ask for the name at "LAUNCH"', async () => {
            const conversation = testSuite.conversation();

            const launchRequest = await testSuite.requestBuilder.launch();
            const responseLaunchRequest = await conversation.send(launchRequest);
            expect(
                responseLaunchRequest.isAsk(launchMeaage)
            ).toBe(true);

        });
    });
}
package.json(追記)
  "scripts":{
    "test": "jest"
  },
$ yarn test
yarn run v1.13.0
$ jest
 PASS  test/sample.test.ts
  PLATFORM: Alexa INTENTS
    ✓ should return a welcome message and ask for the name at "LAUNCH" (40ms)
  PLATFORM: GoogleAssistant INTENTS
    ✓ should return a welcome message and ask for the name at "LAUNCH" (12ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        5.1s
Ran all test suites.
✨  Done in 7.44s.

デプロイ

デプロイは$ jovo deployでプロジェクト配下にあるproject.jsの情報をもとに全プラットフォームにデプロイをしてくれるらしいです。Alexaへのデプロイにはask-cliで事前にask initをしておく必要があります。Googleに関してもIAMでDialogFlowのadminユーザを作成してCloud SDKでクレデンシャルを事前に取得しておく必要があります。詳しくは公式のチュートリアルに書いてます。またオプションを指定することで、コードはデプロイせずにAlexaやDialogFlowの音声設計の部分だけデプロイすることとかもできるようです。

感想

簡単にAlexaとGoogle Assistantの開発環境を整えられて開発ができるのは非常にメリットに感じました。ただ、今後もAlexaもGoogleも本家のSDKをアップデートしていくだろうし、追従していけるかどうか怪しいので業務で使うには少し不安に感じるところです。ただ、DBとのインテグレーションが楽そうだったり、State Managementが便利そうだったりまだまだメリットはたくさんあると思うので、引き続き調査をしてみて機会があれば業務でも使ってみようと思いました。一時間くらいあれば試せるので是非みなさんもJovoを試してみて、Alexa,Google Assistantの両プラットフォームでスキルを作ってみましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Alexa SkillとActions on Googleを同じコードで作るフレームワーク「Jovo」を試してみた

Jovoとは

こんばんは。家に帰って少し暇だったので遊んでみました。JovoとはAlexa SkillとActions on Googleを一つのコードベースで構築できる初のフレームワークです。ちょうど最近ver2.2がリリースされてTypeScriptへのサポートが強化されたとのことだったので触ってみました。今回はサンプルとして簡単な計算をする音声アプリを作ってみます。

インストール

インストールは非常に簡単です。
yarnを利用している場合は下記のコマンドを叩いてjovo-cliをグローバルにインストールしましょう。

$ yarn global add jovo-cli
$ jovo -v
Jovo CLI Version: 2.2.3

プロジェクトの作成

先ほどインストールしたjovo-cliを使用してプロジェクトを作成します。
TypeScriptを使用する場合は、オプションで --language typescriptをつけてください。

$ jovo new <directory> --language typescript

これで前準備が整ったのでまずはmodelを作っていきます。
models配下にen-US.jsonというファイルがあるのでこのファイルをリネームしてja-JP.jsonにします。
そしてこのjsonファイルを使って音声モデルを組み立てます。
今回は簡単な計算をするスキルなので以下のように組み立てました。

ja-JP.json
{
    "invocation": "Sample Jovo app",
    "intents": [
        {
            "name": "PlusIntent",
            "phrases": [
                "{number}を足して",
                "プラス{number}",
                "{number}を足す"
            ],
            "inputs": [
                {
                    "name": "number",
                    "type": {
                        "alexa": "AMAZON.NUMBER",
                        "dialogflow": "@sys.number"
                    }
                }
            ]
        },
        {
            "name": "MinusIntent",
            "phrases": [
                "{number}を引いて",
                "マイナス{number}",
                "{number}を引く"
            ],
            "inputs": [
                {
                    "name": "number",
                    "type": {
                        "alexa": "AMAZON.NUMBER",
                        "dialogflow": "@sys.number"
                    }
                }
            ]
        },

        {
            "name": "ResultAnswerIntent",
            "phrases": [
                "答えは{answer}",
                "{answer}"
            ],
            "inputs": [
                {
                    "name": "answer",
                    "type": {
                        "alexa": "AMAZON.NUMBER",
                        "dialogflow": "@sys.number"
                    }
                }
            ]
        },

        {
            "name": "ResultHelpIntent",
            "phrases": [
                "結果を教えて",
                "答えを教えて"
            ]
        }
    ],
    "alexa": {
        "interactionModel": {
            "languageModel": {
                "intents": [
                    {
                        "name": "AMAZON.CancelIntent",
                        "samples": []
                    },
                    {
                        "name": "AMAZON.HelpIntent",
                        "samples": []
                    },
                    {
                        "name": "AMAZON.StopIntent",
                        "samples": []
                    }
                ]
            }
        }
    },
    "dialogflow": {
        "intents": [
            {
                "name": "Default Fallback Intent",
                "auto": true,
                "webhookUsed": true,
                "fallbackIntent": true
            },
            {
                "name": "Default Welcome Intent",
                "auto": true,
                "webhookUsed": true,
                "events": [
                    {
                        "name": "WELCOME"
                    }
                ]
            }
        ]
    }
    }   

上記を見ればわかると思いますが、Intent内で変数を利用する場合はAlexa,Dialogflowそれぞれのプラットフォームのスロットやエンティティを定義してあげる必要があります。今回は使っていませんがカスタムのスロットやエンティティを利用する場合は下記のように共有で定義してあげればいいようです。もちろんsynonyms(類義語)などにも対応しているようです。


"inputs": [
    {
        "name": "city",
        // Use your own input type
        "type": "myCityInputType"
    }
]
"inputTypes": [
    {
        "name": "myCityInputType",
        "values": [
            {
                "value": "Berlin"
            },
            {
                "value": "New York",
                "synonyms": [
                    "New York City"
                ]
            }
        ]
    }
]

公式ドキュメントを参考に以下のサンプルを作ってみました。

app.ts
import {App} from 'jovo-framework';
import {Alexa} from 'jovo-platform-alexa';
import {JovoDebugger} from 'jovo-plugin-debugger';
import {FileDb} from 'jovo-db-filedb';
import {GoogleAssistant} from 'jovo-platform-googleassistant';

// ------------------------------------------------------------------
// APP INITIALIZATION
// ------------------------------------------------------------------

const app = new App();

app.use(
    new Alexa(),
    new GoogleAssistant(),
    new JovoDebugger(),
    new FileDb(),
);


// ------------------------------------------------------------------
// APP LOGIC
// ------------------------------------------------------------------

app.setHandler({
    LAUNCH() {
        return this.toIntent('WelcomeIntent');
    },

    WelcomeIntent() {
        this.ask("簡単な計算スキルです。足し算と引き算ができます。最初は0から始まります。一を足してや三を引いてなどを私に言ってください。");
    },

    PlusIntent() {
        const plusnumber : number = +this.$inputs.number.value;
        // session_attributesから値を引っ張ってくる
        const currentResult : number = +this.$session.$data.result || 0;
        const result : number = currentResult + plusnumber;
        // session_attributesにデータを格納する
        this.$session.$data.result = result;
        this.ask(plusnumber.toString() + "を足しました。計算をやめる場合は答えを言うか、答えを教えてなどと聞いてください");
    },
    MinusIntent() {
        const minusnumber : number = +this.$inputs.number.value;
        // session_attributesから値を引っ張ってくる
        const currentResult : number = +this.$session.$data.result || 0;
        const result : number = currentResult - minusnumber;
        // session_attributesにデータを格納する
        this.$session.$data.result = result;
        this.ask(minusnumber.toString() + "を引きました。計算をやめる場合は答えを言うか、答えを教えてなどと聞いてください");
    },
    ResultAnswerIntent() {
        const answer : number = +this.$inputs.answer.value;
        const currentResult : number = +this.$session.$data.result || 0;
        console.log(answer, currentResult);
        if (answer === currentResult){
            this.tell("正解です。答えは" + currentResult.toString() + "でした。");
        }
        else{
            this.ask("違います。わからなければ答えを教えてと聞いてみてください。");
        }
    },
    ResultHelpIntent(){
        const currentResult : number = +this.$session.$data.result || 0;
        this.tell("答えは" + currentResult.toString() + "でした。");
    }
});

export {app};

テスト

サンプルコードができたので以下のコマンドでローカルでコンパイルして起動をしてみます。

$ npm run tsc
$ jovo run

起動後に「.」を押すとブラウザ上でデバッガーを起動できます。
jovo_debug1.mov.gif

このデバッガーがかなり便利で、直感的にAlexaとGoogle Assistantの両方でマニュアルテストをすることができます。

$ npm run tscw$ jovo run --watchで実行すれば、ホットリロードをしてくれるのでスピード感を持った開発ができそうですね。
しかし、あくまで、このツールできるのは「文字でのテスト」になるので、実際に喋った時と同じ挙動をするかはまた別の話です。音声アプリを作る際には実機での検証が大切です。

また、ユニットテストをする際にはjestを利用するといいと思います。下記は起動リクエストを飛ばした時に正常なレスポンスが返ってくるかのテストです。

sample_test.ts
import {Alexa} from 'jovo-platform-alexa';
import {GoogleAssistant} from 'jovo-platform-googleassistant';

jest.setTimeout(500);

const launchMeaage = "簡単な計算スキルです。足し算と引き算ができます。最初は0から始まります。一を足してや三を引いてなどを私に言ってください。";

for (const p of [new Alexa(), new GoogleAssistant()]) {
    const testSuite = p.makeTestSuite();

    describe(`PLATFORM: ${p.constructor.name} INTENTS` , () => {
        test('should return a welcome message and ask for the name at "LAUNCH"', async () => {
            const conversation = testSuite.conversation();

            const launchRequest = await testSuite.requestBuilder.launch();
            const responseLaunchRequest = await conversation.send(launchRequest);
            expect(
                responseLaunchRequest.isAsk(launchMeaage)
            ).toBe(true);

        });
    });
}
package.json(追記)
  "scripts":{
    "test": "jest"
  },
$ yarn test
yarn run v1.13.0
$ jest
 PASS  test/sample.test.ts
  PLATFORM: Alexa INTENTS
    ✓ should return a welcome message and ask for the name at "LAUNCH" (40ms)
  PLATFORM: GoogleAssistant INTENTS
    ✓ should return a welcome message and ask for the name at "LAUNCH" (12ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        5.1s
Ran all test suites.
✨  Done in 7.44s.

デプロイ

デプロイは$ jovo deployでプロジェクト配下にあるproject.jsの情報をもとに全プラットフォームにデプロイをしてくれるらしいです。Alexaへのデプロイにはask-cliで事前にask initをしておく必要があります。Googleに関してもIAMでDialogFlowのadminユーザを作成してCloud SDKでクレデンシャルを事前に取得しておく必要があります。詳しくは公式のチュートリアルに書いてます。またオプションを指定することで、コードはデプロイせずにAlexaやDialogFlowの音声設計の部分だけデプロイすることとかもできるようです。

感想

簡単にAlexa SkillとActions on Googleの開発環境を整えられて開発ができるのは非常にメリットに感じました。ただ、今後もAmazonもGoogleも本家のSDKをアップデートしていくだろうし、追従していけるかどうか怪しいので業務で使うには少し不安に感じるところです。ただ、DBとのインテグレーションが楽そうだったり、State Managementが便利そうだったりまだまだメリットはたくさんあると思うので、引き続き調査をしてみて機会があれば業務でも使ってみようと思いました。一時間くらいあれば試せるので是非みなさんもJovoを試してみて、Alexa,Google Assistantの両プラットフォームでスキルを作ってみましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel5.4 ファイル複数ダウンロード

ファイルダウンロード処理の実装

主な方法
・Response::download
(サーバー上のファイルダウンロード)(IEだと文字化け可能性)
・Response::make()
(文字化けはないが、ファイル内容をキャストしてコピーするので、大容量には向かない)
・Storage::disk()->downlaod (バージョン的に使えない)
・phpの処理で作る (カスタマイズできる)

ダウンロード参考記事
大容量・laravel・phpファイルダウンロード参考記事

今回は、ファイル内容はそんなに重くなく、また既にファイルの中身をバイナリデータで持っていたので、Response::make()を使用することにした。
(Storage->disk('s3')->get(ファイルパス)でバイナリデータは取得していた。)

$headers = [
                'Content-Type' => $mineType,
                'Content-Disposition' => 'attachment; filename="' . $fileName . '"'
           ];
return Response::make($fileBinaryData, 200, $headers);

これでファイルダウンロードは完了。

複数ダウンロードを実装する

{{Form::button('ダウンロード', ['class' => 'on-download'])}}
<script>
$('.on-download').on('click', function(){
    handleDownload("samplel1.jpg");
    handleDownload("samplel2.jpg");
    handleDownload("samplel3.jpg");
});

function handleDownload(name) {

    const route = "{{route(download)}}";

    // IE
    if (window.navigator.msSaveBlob) {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", route, true);
        xhr.responseType = "blob";
        xhr.onload = function (e) {
            var blob = xhr.response;
            window.navigator.msSaveBlob(blob, name);
        }
        xhr.send();
        return;
    }

    // chrome,firefox
    const a = document.createElement('a');
    a.download = name;
    a.href = route;

    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);


}
</script>

aタグのdownload属性を使用して、ダウンロード処理を実装する。download属性を持つaタグを生成してclickで実行させ、消すを繰り返す.
IEはdownload属性をサポートしていないので、一つのファイルがダウンロードされるくらいなので、IE用に処理を分岐させる。ダウンロードする画像数分タブが表示されては消えるようにしているため、msSaveBlobで保存する。

実践的に使うには

僕が今回実際に使ったときは、テーブルの行ごとの先頭にチェックボックスが存在し、チェックが入ったレコードのファイルだけをダウンロードさせることをする必要があった。

なので、checkboxにdata属性でfileIdとfileNameを渡す必要があった。ダウンロードボタンを押した瞬間に、checkが入ってるもののチェックボックスからfileIdとfileNameを取り出して、それをeach文で、handleDownload(fileId, fileName)(fileIdはrouteを生成するときにどのファイルをダウンロードさせるかを判定するためにくっつけて渡す)を複数回実行させることで、課題をクリアした。

aタグのdownload属性を使って、直接ファイルをダウンロードさせる方法もあるが、僕の場合ダウンロード履歴をDBに保存する必要があったため、いったんスクリプトを経由した。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel5.4 ファイル複数ダウンロード(画像でもテキストでも)

ファイルダウンロード処理の実装

主な方法
・Response::download
(サーバー上のファイルダウンロード)(IEだと文字化け可能性)
・Response::make()
(文字化けはないが、ファイル内容をキャストしてコピーするので、大容量には向かない)
・Storage::disk()->downlaod (バージョン的に使えない)
・phpの処理で作る (カスタマイズできる)

ダウンロード参考記事
大容量・laravel・phpファイルダウンロード参考記事

今回は、ファイル内容はそんなに重くなく、また既にファイルの中身をバイナリデータで持っていたので、Response::make()を使用することにした。
(Storage->disk('s3')->get(ファイルパス)でバイナリデータは取得していた。)

$headers = [
                'Content-Type' => $mineType,
                'Content-Disposition' => 'attachment; filename="' . $fileName . '"'
           ];
return Response::make($fileBinaryData, 200, $headers);

これでファイルダウンロードは完了。

複数ダウンロードを実装する

{{Form::button('ダウンロード', ['class' => 'on-download'])}}
<script>
$('.on-download').on('click', function(){
    handleDownload("samplel1.jpg");
    handleDownload("samplel2.jpg");
    handleDownload("samplel3.jpg");
});

function handleDownload(name) {

    const route = "{{route(download)}}";

    // IE
    if (window.navigator.msSaveBlob) {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", route, true);
        xhr.responseType = "blob";
        xhr.onload = function (e) {
            var blob = xhr.response;
            window.navigator.msSaveBlob(blob, name);
        }
        xhr.send();
        return;
    }

    // chrome,firefox
    const a = document.createElement('a');
    a.download = name;
    a.href = route;

    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);


}
</script>

aタグのdownload属性を使用して、ダウンロード処理を実装する。download属性を持つaタグを生成してclickで実行させ、消すを繰り返す.
IEはdownload属性をサポートしていないので、一つのファイルがダウンロードされるくらいなので、IE用に処理を分岐させる。ダウンロードする画像数分タブが表示されては消えるようにしているため、msSaveBlobで保存する。

今回こうやって使用した

僕が今回実際に使ったときは、テーブルの行ごとの先頭にチェックボックスが存在し、チェックが入ったレコードのファイルだけをダウンロードさせるというものだった。

なので、checkboxにdata属性でfileIdとfileNameを渡す必要があった。ダウンロードボタンを押した瞬間に、checkが入ってるもののチェックボックスからfileIdとfileNameを取り出して、それをeach文で、handleDownload(fileId, fileName)(fileIdはrouteを生成するときにどのファイルをダウンロードさせるかを判定するためにくっつけて渡す)を複数回実行させることで、課題をクリアした。

aタグのdownload属性を使って、直接ファイルをダウンロードさせる方法もあるが、僕の場合ダウンロード履歴をDBに保存する必要があったため、いったんスクリプトを経由した。

参考記事

まとめ

複数画像ファイルをダウンロードさせることにすごいはまった。
つぎからはいけそう。
今度はダウンロード後を検知する方法を知りたいなぁ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む