20190215のRubyに関する記事は24件です。

Rubyでnilか空文字を判定する

nil、空文字の判定方法

nilの判定方法

check_nil.rb
str = nil

if str.nil?
  puts "strはnilです。"
end

# strはnilです。

空文字の判定方法

check_empty.rb
str = ""

if str.empty?
  puts "strは空文字です。"
end

# strは空文字です。

nilまたは空文字を判定する

nilと空文字を同時に判定する方法

is_nil_or_empty.rb
def is_nil_or_empty?(str)
  if str.nil? || str.empty?
    return true
  else
    return false
  end
end

check_array = Array.new
check_array.push("")
check_array.push(nil)

check_array.each do |str|
  if is_nil_or_empty?(str)
    puts "strはnilまたは空文字です。"
  end
end

#strはnilまたは空文字です。
#strはnilまたは空文字です。

注意点

if式の条件式は str.nil? || str.empty? とすべきです。
理由は、||演算子は左から右に順に条件式を評価し、結果が真になった時点でif式の中に入りますが、
empty?から先に評価した場合、対象の文字列がnilであればNoMethodErrorを返すためです。
empty?は文字列、配列、ハッシュのみに対応しています。nilには対応していないため、NoMethodErrorが返ってきます。
nil?は全てのオブジェクトに対応しているため、if式の条件式はnil?を左に書くべきです。

no_method_error.rb
def is_nil_or_empty?(str)
  if str.empty? || str.nil?
    return true
  else
    return false
  end
end

check_array = Array.new
check_array.push("")
check_array.push(nil)

check_array.each do |str|
  if is_nil_or_empty?(str)
    puts "strはnilまたは空文字です。"
  end
end

#undefined method `empty?' for nil:NilClass (NoMethodError)

余談

to_sメソッドを使って文字列型に変換することで、nilと空文字を判定することも出来ます。

use_to_s.rb
def is_nil_or_empty?(str)
  if str.to_s.empty?
    return true
  else
    return false
  end
end

check_array = Array.new
check_array.push("")
check_array.push(nil)

check_array.each do |str|
  if is_nil_or_empty?(str)
    puts "strはnilまたは空文字です。"
  end
end

#strはnilまたは空文字です。
#strはnilまたは空文字です。

ただし、UTF-8 でしか動かない処理に投入する場合は、上記の判定方法は使ってはいけない。
https://qiita.com/scivola/items/1f6704f81aba18df9012

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

Rubyでnilか空文字を判定する方法

nil、空文字の判定

nilであるかを判定

check_nil.rb
str = nil

if str.nil?
  puts "strはnilです。"
end

# strはnilです。

空文字であるかを判定

check_empty.rb
str = ""

if str.empty?
  puts "strは空文字です。"
end

# strは空文字です。

nilまたは空文字かどうかを判定する

nilと空文字を判定する方法

is_nil_or_empty.rb
def is_nil_or_empty?(str)
  if str.nil? || str.empty?
    return true
  else
    return false
  end
end

check_array = Array.new
check_array.push("")
check_array.push(nil)

check_array.each do |str|
  if is_nil_or_empty?(str)
    puts "strはnilまたは空文字です。"
  end
end

#strはnilまたは空文字です。
#strはnilまたは空文字です。

注意点

if式の条件式は str.nil? || str.empty? とすべきです。
理由は、||演算子は左から右に順に条件式を評価し、結果が真になった時点でif式の中に入りますが、
empty?から先に評価した場合、対象の文字列がnilであればNoMethodErrorを返すためです。
empty?は文字列、配列、ハッシュのみに対応しています。nilには対応していないため、NoMethodErrorが返ってきます。
nil?は全てのオブジェクトに対応しているため、if式の条件式はnil?を左に書くべきです。

no_method_error.rb
def is_nil_or_empty?(str)
  if str.empty? || str.nil?
    return true
  else
    return false
  end
end

check_array = Array.new
check_array.push("")
check_array.push(nil)

check_array.each do |str|
  if is_nil_or_empty?(str)
    puts "strはnilまたは空文字です。"
  end
end

#undefined method `empty?' for nil:NilClass (NoMethodError)

Ruby on Railsの場合

blank?を使用することで、nilと空文字を評価することが出来ます。
なお、blank?の否定(nilかつ空文字でない)はpresent?を使用します。

ruby_on_rails.rb
def is_nil_or_empty?(str)
  if str.blank?
    return true
  else
    return false
  end
end

check_array = Array.new
check_array.push("")
check_array.push(nil)

check_array.each do |str|
  if is_nil_or_empty?(str)
    puts "strはnilまたは空文字です。"
  end
end

#strはnilまたは空文字です。
#strはnilまたは空文字です。

余談

to_sメソッドを使って文字列型に変換することで、nilと空文字を評価することも出来ます。

use_to_s.rb
def is_nil_or_empty?(str)
  if str.to_s.empty?
    return true
  else
    return false
  end
end

check_array = Array.new
check_array.push("")
check_array.push(nil)

check_array.each do |str|
  if is_nil_or_empty?(str)
    puts "strはnilまたは空文字です。"
  end
end

#strはnilまたは空文字です。
#strはnilまたは空文字です。

ただし、UTF-8 でしか動かない処理に投入する場合は、上記の方法は使ってはいけない。
https://qiita.com/scivola/items/1f6704f81aba18df9012

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

Rubyでnilか空文字列を判定する方法

nil、空文字列の判定

nilであるかを判定

check_nil.rb
str = nil

if str.nil?
  puts "strはnilです。"
end

# strはnilです。

空文字列であるかを判定

check_empty.rb
str = ""

if str.empty?
  puts "strは空文字列です。"
end

# strは空文字列です。

nilまたは空文字列かどうかを判定する

nilと空文字列を判定する方法

is_nil_or_empty.rb
def is_nil_or_empty?(str)
  str.nil? || str.empty?
end

check_array = ["", nil]

check_array.each do |str|
  if is_nil_or_empty?(str)
    puts "strはnilまたは空文字列です。"
  end
end

#strはnilまたは空文字列です。
#strはnilまたは空文字列です。

注意点

if式の条件式は str.nil? || str.empty? とすべきです。
理由は、||演算子は左から右に順に条件式を評価し、結果が真になった時点でif式の中に入りますが、
empty?から先に評価した場合、対象の文字列がnilであればNoMethodErrorを返すためです。
empty?は文字列、配列、ハッシュのみに対応しています。nilには対応していないため、NoMethodErrorが返ってきます。
nil?は全てのオブジェクトに対応しているため、if式の条件式はnil?を左に書くべきです。

no_method_error.rb
def is_nil_or_empty?(str)
  str.empty? || str.nil?
end

check_array = ["", nil]

check_array.each do |str|
  if is_nil_or_empty?(str)
    puts "strはnilまたは空文字列です。"
  end
end

#undefined method `empty?' for nil:NilClass (NoMethodError)

Ruby on Railsの場合

blank?を使用することで、nilと空文字列を評価することが出来ます。
blank? はnilや空文字列以外に「空白文字列のみからなる文字列」もtrueを返すメソッドであるとのこと。
「' '.blank?」や「\t\n\r".blank?」もtrueを返すため、例えば全角空白をfalseとしたい場合は注意が必要です。
なお、blank?の否定はpresent?を使用します。

ruby_on_rails.rb
def is_nil_or_empty?(str)
  str.blank?
end

check_array = ["", nil]

check_array.each do |str|
  if is_nil_or_empty?(str)
    puts "strはnilまたは空文字列です。"
  end
end

#strはnilまたは空文字列です。
#strはnilまたは空文字列です。

余談

to_sメソッドを使って文字列型に変換することで、nilと空文字列を評価することも出来ます。

use_to_s.rb
def is_nil_or_empty?(str)
  str.to_s.empty?
end

check_array = ["", nil]

check_array.each do |str|
  if is_nil_or_empty?(str)
    puts "strはnilまたは空文字列です。"
  end
end

#strはnilまたは空文字列です。
#strはnilまたは空文字列です。

ただし、UTF-8 でしか動かない処理に投入する場合は、上記の方法は使ってはいけない。
https://qiita.com/scivola/items/1f6704f81aba18df9012

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

後から名前空間を切ったらPG::UndefinedTable with namespaced Modelが発生した

API作ったが、後から名前空間を別に切り出すことになった

API完成したものの、各モジュールの名前が煩雑になりすぎたので後から名前空間を切り出すことになった。

一通り変えたけどDBでエラー吐いてる

一通り変えて、念のためテストを走らす。
ちゃんとモデル名も変更してるし、DBと合ってるな。

。。。

PG::UndefinedTable with namespaced Model

DBがエラー吐いてるやん!

原因

prefixを定義してなかったのが原因でした。
例えば以下の様に名前空間をHogeで切っていて、その中にUserというモデルを定義しており、テーブル名はhoge_usersで定義されていた場合。

module Hoge
  class User < ApplicationRecord
  end
end

DBに問い合わせるにはtable_name_prefixというメソッドを定義してやると、ちゃんと頭にhogeを付けてDBに問い合わせてくれるので、テーブルが見つからないと言われなくなります。

module Hoge
  def self.table_name_prefix
    'hoge_'
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SQLの集計結果をクロス表に変形するrubyスクリプト

はじめに

SQLでクロス集計形式にするのって結構大変ですよね?
ということで、単純なSQLの集計クエリの結果をクロス集計に変形するrubyスクリプトを作りました。

https://bitbucket.org/cnaos/cross-tab/overview

使い方

SQLの集計結果をTSVファイルとして出力したファイルを
パイプでcross-tab.rbに食わせると
クロス集計型に変形して標準出力に出力します。

cat sample.tsv | ./cross-tab.rb 

サンプルとして使うデータ

mysqlのSakila Sample Databaseのpaymentテーブルのデータを使います。
https://dev.mysql.com/doc/sakila/en/sakila-structure-tables-payment.html

paymentテーブル
mysql> desc payment;
+--------------+----------------------+------+-----+-------------------+-----------------------------+
| Field        | Type                 | Null | Key | Default           | Extra                       |
+--------------+----------------------+------+-----+-------------------+-----------------------------+
| payment_id   | smallint(5) unsigned | NO   | PRI | NULL              | auto_increment              |
| customer_id  | smallint(5) unsigned | NO   | MUL | NULL              |                             |
| staff_id     | tinyint(3) unsigned  | NO   | MUL | NULL              |                             |
| rental_id    | int(11)              | YES  | MUL | NULL              |                             |
| amount       | decimal(5,2)         | NO   |     | NULL              |                             |
| payment_date | datetime             | NO   |     | NULL              |                             |
| last_update  | timestamp            | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+--------------+----------------------+------+-----+-------------------+-----------------------------+
7 rows in set (0.00 sec)

例1:集計項目が1つだけの場合

paymentテーブルのデータをpayment_dateで日付ごとに集計して、
staff_idごとにその日の支払い件数の合計を出します。

サンプルクエリ1
SELECT 
  DATE_FORMAT(payment_date,'%Y-%m-%d') date, 
  staff_id, 
  count(*) 
FROM payment 
GROUP BY date,staff_id;
クエリ結果1(抜粋)
+------------+----------+----------+
| date       | staff_id | count(*) |
+------------+----------+----------+
| 2005-05-24 |        1 |        4 |
| 2005-05-24 |        2 |        4 |
| 2005-05-25 |        1 |       73 |
| 2005-05-25 |        2 |       64 |
| 2005-05-26 |        1 |       96 |
| 2005-05-26 |        2 |       78 |
| 2005-05-27 |        1 |       84 |
| 2005-05-27 |        2 |       83 |
| 2005-05-28 |        1 |      119 |
| 2005-05-28 |        2 |       77 |

サンプルクエリ1の結果をTSVファイルに出力したのが、sample-data/sample1.tsv です。

sample-data/sample1.tsv
date    staff_id    count(*)
2005-05-24  1   4
2005-05-24  2   4
2005-05-25  1   73
2005-05-25  2   64
2005-05-26  1   96
2005-05-26  2   78
2005-05-27  1   84
2005-05-27  2   83
2005-05-28  1   119
2005-05-28  2   77

このファイルを作成したcross-tabスクリプトに通すと以下のようになります。

head -11 sample-data/sample1.tsv | ./cross-tab.rb
処理結果1
date    1_count(*)  2_count(*)
2005-05-24  4   4
2005-05-25  73  64
2005-05-26  96  78
2005-05-27  84  83
2005-05-28  119 77

例2:集計項目が2つある場合

例1では
payment_dateで日付ごとに集計して、
staff_idごとにその日の支払い件数の合計を出しましたが、
さらに支払い金額(amount)の合計も集計したくなったとしましょう。

集計用のクエリを以下のように変更しました。

サンプルクエリ2
SELECT 
  DATE_FORMAT(payment_date,'%Y-%m-%d') date, 
  staff_id, 
  count(*), 
  sum(amount) 
FROM payment 
GROUP BY date,staff_id;
クエリ結果2(抜粋)
+------------+----------+----------+-------------+
| date       | staff_id | count(*) | sum(amount) |
+------------+----------+----------+-------------+
| 2005-05-24 |        1 |        4 |       15.96 |
| 2005-05-24 |        2 |        4 |       13.96 |
| 2005-05-25 |        1 |       73 |      323.27 |
| 2005-05-25 |        2 |       64 |      250.36 |
| 2005-05-26 |        1 |       96 |      401.04 |
| 2005-05-26 |        2 |       78 |      353.22 |
| 2005-05-27 |        1 |       84 |      357.16 |
| 2005-05-27 |        2 |       83 |      328.17 |
| 2005-05-28 |        1 |      119 |      480.81 |
| 2005-05-28 |        2 |       77 |      323.23 |

サンプルクエリ2の結果をTSVファイルに出力したのが、sample-data/sample2.tsv です。

このファイルも同様にcross-tabスクリプトに通すと以下のようになります。

head -11 sample-data/sample2.tsv | ./cross-tab.rb
処理結果2
date    1_count(*)  1_sum(amount)   2_count(*)  2_sum(amount)
2005-05-24  4   15.96   4   13.96
2005-05-25  73  323.27  64  250.36
2005-05-26  96  401.04  78  353.22
2005-05-27  84  357.16  83  328.17
2005-05-28  119 480.81  77  323.23
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails sできない時の対処法

※初学者向けです。

Ruby on Railsで Rails serverコマンドをした時にサーバーが立ち上がらなかった時に対処したことをメモ。

エラー内容

rails sとしてローカル(http://192.168.33.10:3000/ )にアクセスすると,

ActiveRecord::ConnectionNotEstablished
No connection pool with 'primary' found.

といったようなメッセージが出て上手く動いていない。

ActiveRecordとはRuby on Railsにおいてデータベースとのやりとりに使われているフレームワーク?です。
なのでこのエラーはデータベース関連のエラーであると推測されます。

Rails db:migrateで確認

ターミナルを見返すとそもそもdb:migrateが上手くいってません

rails db:migrate
とすると
Gem::LoadError: can't activate sqlite3 (~> 1.3.6), already activated sqlite3-1.4.0. Make sure all dependencies are added to Gemfile.
というエラー。

内容はGemのロードエラー。具体的にはsqlite3 (~> 1.3.6)がアクティベートできないけどsqlite3 (1.4.0)はアクティベートされてるよ、というもの。

おそらくRailsが最新版のsqlite3(1.4.0)を受け付けてないと推測。
ちなみに(~>1.3.6)とは '1.3.6以上1.4.0未満'という意味。

対処法:Gemfileの更新

Rails new したフォルダにあるGemfileの内容を変えましょう。
(初学者の私にとっては「???」状態でしたが。。。)

Gemfile
の中にある
gem 'sqlite3'
という部分を
gem 'sqlite3', '~> 1.3.6'
に変更しましょう。

これで
bundle update
bundle install
というコマンドをターミナル上で実行し、
rails s

晴れてRuby on Railsのデフォルト画面が現れるはず。

初学者でいきなり詰まったところだったのでメモを残します。

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

比較演算子

比較演算子


a == b # a と b が等しい
a != b # a と b が等しくない
a < b # a が b よりも小さい
a > b # a が b よりも大きい
a <= b # a が b 以下である
a >= b # a が b 以上である

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

変数展開

変数展開

変数展開とは...
変数を代入している値に置き換えて、文字列の中に含めること

name = '佐藤'
puts "こんにちは#{name}さん"

ダブルクォーテーション「""」じゃないと変数展開できないので注意!

数値と文字列を足し算で連結することはできないが、
変数展開を使えば、数値の入った変数と文字列を連結することができる。

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

boxからファイル一覧を取得するLambda関数をServerless Frameworkで構築する

なにこれ

連携会社や他のチームと実績データや素材データをやりとりすることがままあります。
今回はその一例として、「閲覧権限のあるフォルダに定期的にファイルが追加されるので、それを利用して色々処理する定期バッチをLambdaで構築したい」というお話です。
あと、折角なのでServerless Frameworkを使ってみました。
その手順をまとめます。

構成図

image.png
こんな感じ。
今回は機微な話なのでストレージ保存とかは割愛して取得するところまでの手順を書きます。

Serverless Framework

デプロイはServerless Frameworkで行います。
Serverless Framework自体の詳細な説明は、公式でも手厚くしてくれているし記事も沢山あるのでここでは省きます。
ザックリ雑には「CloudFormationとかTerraformで頑張らなくてもコマンド一つでポンとデプロイできるイカしたフレームワーク」という認識で問題ないかと思います。

「Serverless Frameworkは一旦いらない」「boxの方説明して」という人はこの節はかっ飛ばしてください。

環境設定

事前準備として以下が設定済みであることと仮定します。

まずはインストール。
npmコマンド一つで簡単に使えるようになります

$ npm install -g serverless

インストールが終わったらslsと叩いてみましょう(デカイので折りたたんでます)。


展開して中身を確認 :eyes:
$ sls

Commands
* You can run commands with "serverless" or the shortcut "sls"
* Pass "--verbose" to this command to get in-depth plugin info
* Pass "--no-color" to disable CLI colors
* Pass "--help" after any <command> for contextual help

Framework
* Documentation: https://serverless.com/framework/docs/

config ........................ Configure Serverless
config credentials ............ Configures a new provider profile for the Serverless Framework
create ........................ Create new Serverless service
install ....................... Install a Serverless service from GitHub or a plugin from the Serverless registry
package ....................... Packages a Serverless service
deploy ........................ Deploy a Serverless service
deploy function ............... Deploy a single function from the service
deploy list ................... List deployed version of your Serverless Service
deploy list functions ......... List all the deployed functions and their versions
invoke ........................ Invoke a deployed function
invoke local .................. Invoke function locally
info .......................... Display information about the service
logs .......................... Output the logs of a deployed function
metrics ....................... Show metrics for a specific function
print ......................... Print your compiled and resolved config file
remove ........................ Remove Serverless service and all resources
rollback ...................... Rollback the Serverless service to a specific deployment
rollback function ............. Rollback the function to the previous version
slstats ....................... Enable or disable stats
plugin ........................ Plugin management for Serverless
plugin install ................ Install and add a plugin to your service
plugin uninstall .............. Uninstall and remove a plugin from your service
plugin list ................... Lists all available plugins
plugin search ................. Search for plugins

Plugins
AwsConfigCredentials, Config, Create, Deploy, Info, Install, Invoke, Logs, Metrics, Package, Plugin, PluginInstall, PluginList, PluginSearch, PluginUninstall, Print, Remove, Rollback, SlStats

プロジェクトの初期化

プロジェクトディレクトリを作成し初期化します。
初期化は、sls createにパラメータとしてランタイム、関数名を指定するだけでOKです。
今回はrubyでbox-listという関数を作成します。

$ mkdir box_list
$ cd box_list/
$ sls create -t aws-ruby --name box-list
Serverless: Generating boilerplate...
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.37.1
 -------'

Serverless: Successfully generated boilerplate for template: "aws-ruby"

実行すると何やら色々画面に出て、対象のディレクトリに設定ファイルが配置されます。

$ ll ~/box_list/
total 16
-rw-r--r--  1 maruta-hirokazu  staff   151B  2 14 16:34 handler.rb
-rw-r--r--  1 maruta-hirokazu  staff   2.8K  2 14 16:34 serverless.yml

中身はこんな感じ(デフォルトがこれまたデカイので折りたたんでます)


展開して中身を確認 :eyes:
$ cat ./handler.rb
require 'json'

def hello(event:, context:)
  { statusCode: 200, body: JSON.generate('Go Serverless v1.0! Your function executed successfully!') }
end
$
$ cat  serverless.yml
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
#    docs.serverless.com
#
# Happy Coding!

service: box-list # NOTE: update this with your service name

# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
# frameworkVersion: "=X.X.X"

provider:
  name: aws
  runtime: ruby2.5

# you can overwrite defaults here
#  stage: dev
#  region: us-east-1

# you can add statements to the Lambda function's IAM Role here
#  iamRoleStatements:
#    - Effect: "Allow"
#      Action:
#        - "s3:ListBucket"
#      Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ]  }
#    - Effect: "Allow"
#      Action:
#        - "s3:PutObject"
#      Resource:
#        Fn::Join:
#          - ""
#          - - "arn:aws:s3:::"
#            - "Ref" : "ServerlessDeploymentBucket"
#            - "/*"

# you can define service wide environment variables here
#  environment:
#    variable1: value1

# you can add packaging information here
#package:
#  include:
#    - include-me.py
#    - include-me-dir/**
#  exclude:
#    - exclude-me.py
#    - exclude-me-dir/**

functions:
  hello:
    handler: handler.hello

#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
#    events:
#      - http:
#          path: users/create
#          method: get
#      - s3: ${env:BUCKET}
#      - schedule: rate(10 minutes)
#      - sns: greeter-topic
#      - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
#      - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
#      - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
#      - iot:
#          sql: "SELECT * FROM 'some_topic'"
#      - cloudwatchEvent:
#          event:
#            source:
#              - "aws.ec2"
#            detail-type:
#              - "EC2 Instance State-change Notification"
#            detail:
#              state:
#                - pending
#      - cloudwatchLog: '/aws/lambda/hello'
#      - cognitoUserPool:
#          pool: MyUserPool
#          trigger: PreSignUp

#    Define function environment variables here
#    environment:
#      variable2: value2

# you can add CloudFormation resource templates here
#resources:
#  Resources:
#    NewResource:
#      Type: AWS::S3::Bucket
#      Properties:
#        BucketName: my-new-bucket
#  Outputs:
#     NewOutput:
#       Description: "Description for the output"
#       Value: "Some output value"

とりあえずワチャワチャ書いてあるのですが、コメントを全部消してみるとこれしか記述がありません。

service: box-list

provider:
  name: aws
  runtime: ruby2.5

functions:
  hello:
    handler: handler.hello

読み解いていくと
* サービス名: box-list
* ランタイム: ruby 2.5
* 実行時の関数: handler.hello
という設定が書かれています。

これだけでデプロイの準備がOKになってしまいます。
すごいよね。
で、対象のhandlerファイルは以下。

require 'json'

def hello(event:, context:)
  { statusCode: 200, body: JSON.generate('Go Serverless v1.0! Your function executed successfully!') }
end

定義ファイル側で指定したファイル名.ハンドラ名にし、(event:, context:)を引数とするだけで動いてくれます。

プロジェクトのデプロイ

先ほど初期化した状態でserverless deployをしてあげるだけでデプロイ完了です。
また、以下のようにprofileを指定することで環境の切り替えもできます。
(何も指定しない場合はdefaultのプロファイルが利用されます)

$ sls deploy --aws-profile myenv

これによって間違って会社のアカウントにLambda関数作っちゃって、あとで「ねぇ、知ってたらいいんだけどこれ何かわかる?・・・あ、君の?いや、勉強に使うのはとてもいいことなんだけど、試し打ち用のアカウントあるんだからさ。まぁ確かに最近そっちの利用料がちょっと高いかなとは思っていたんだけど、社内の方にあげるのは流石にやめてね。」ってチームメンバーに諭される心配もなくなりますね!

で、デプロイを実行すると以下のようにモジャモジャターミナルが動きあっという間に完了してしまいます:tada:

Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service box-list.zip file to S3 (266 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.......................
Serverless: Stack update finished...
Service Information
service: box-list
stage: dev
region: us-east-1
stack: box-list-dev
resources: 5
api keys:
  None
endpoints:
  None
functions:
  hello: box-list-dev-hello
layers:
  None
Serverless: Removing old service artifacts from S3...

マネジメントコンソール上ではこんな感じ。
スクリーンショット 2019-02-14 17.38.34.png
詳細。
スクリーンショット 2019-02-14 17.40.00.png
ばっちり。

なお、デプロイは「プロジェクトフォルダにあるもの全てがzipに圧縮されてアップロードされる」という方式なので、必要なパッケージ(rubyだとgem周りとか)はプロジェクトフォルダ内に全部入れてあげる必要があります。

Box API

本題。
Boxにあるファイルを取得していいようにいじくりまわします。
今回はrubyを使うのでBoxrという公式のgemを利用します。

Boxerのインストール

プロジェクトファイル内で以下を実行。

$ bundle init
Writing new Gemfile to /Users/maruta-hirokazu/box_list/Gemfile
$ echo "gem 'boxr'" >> Gemfile 
$ cat Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# gem "rails"
gem 'boxr'
$ bundle install --path vendor/bundle

以上。

Box側でアプリの作成

アプリを作成しないことにはAPIは利用できないので、まずはBoxアプリを作成します。

開発者コンソールにアクセスします
スクリーンショット 2019-02-14 18.10.33.png

カスタムアプリを選択。
スクリーンショット 2019-02-14 18.13.48.png

認証設定を選びます。
ここについてですが、ユーザー認証はバッチ処理には向かないし、値を環境変数か何かに持たせるだけで認証できた方が便利やろっていうところからJWTを利用します。
スクリーンショット 2019-02-14 18.14.58.png

アプリ名はユニークであればなんでもOKです。
スクリーンショット 2019-02-14 18.26.52.png
ここまでで「アプリの作成」を実行するとアプリができます(できるまでに結構時間がかかります)。
完了すると以下のような画面が出るので、書かれているコマンドを実行してみてください。

スクリーンショット 2019-02-14 18.30.33.png

curl https://api.box.com/2.0/folders/0 -H "Authorization: Bearer ***" -vvv | jq .

200OK でファイル情報が返ってくればとりあえず準備OKです。

アプリの構成設定

デフォルトだと構成はこんな感じになっています(長い)

Developerトークンを利用することで一時的なアクセスとかもできるのですが、今回は認証キーペアを作成してしまいます。
公開キーの追加と管理公開/秘密キーペアを生成を押してください。


注意:2段階認証が必要です

2段階認証してないとこんな感じ怒られます
スクリーンショット 2019-02-14 18.55.37.png

ちなみに、2段階認証で日本探すのは結構大変です
スクリーンショット 2019-02-14 18.59.55.png

キーペアの生成ボタンを押すと秘密鍵がダウンロードされます(今回は資料としてダウンロード画面を出したかったためfirefox使っていますが、ブラウザはなんでもいいです)。
スクリーンショット 2019-02-14 19.02.57.png

中身はこんな感じです。

{
  "boxAppSettings": {
    "clientID": "***",
    "clientSecret": "***",
    "appAuth": {
      "publicKeyID": "***",
      "privateKey": "-----BEGIN ENCRYPTED PRIVATE KEY-----\n******-----END ENCRYPTED PRIVATE KEY-----\n",
      "passphrase": "***"
    }
  },
  "enterpriseID": "***"
}

構成の設定自体はこれでOKで、boxerではこのjsonにある情報を利用します。
なのでキーをなくさないように注意。

Boxの認証設定

ユーザー側からアプリを認証します。
この認証は2種類あるので要件に合わせて対応を変えてください。

  • 全てのユーザーから認証させる場合
  • アプリユーザーからのみ認証させる場合

全てのユーザーから認証させる場合

こちらの方が設定自体は簡単です。

アプリの構成で[アプリケーションアクセス]Enterpriseに指定します。
スクリーンショット 2019-02-15 17.57.55.png
ここのチェックが外れるので入れなおしてください。

[管理コンソール][Enterprise設定][アプリ][カスタムアプリケーション]
スクリーンショット 2019-02-14 20.03.37.png
新しいアプリケーションを承認を押します。
ポップアップするのでクライアントIDを入れます
スクリーンショット 2019-02-14 20.03.47.png
アクセス対象ユーザーすべてのユーザーになっていればOKです。
スクリーンショット 2019-02-15 17.43.09.png
こちらの場合は設定は以上です。

アプリユーザーからのみ認証させる場合

boxにはアプリユーザーという概念があります。
詳細の説明は公式に任せますが、端的にはより堅牢なパターンです。
ただし手順が複雑になります。

アプリの構成で[アプリケーションアクセス]アプリケーションに指定します
スクリーンショット 2019-02-15 17.57.47.png

[管理コンソール][Enterprise設定][アプリ][カスタムアプリケーション]
新しいアプリケーションを承認を押します(この処理自体はすべてのユーザーの場合と同じ)。
スクリーンショット 2019-02-14 20.03.54.png
ポップアップされるものが[このアプリのApp Usersのみ]になっていることに注意してください。
こちらのケースではAdminユーザーからアクセスした場合Cannot obtain user token based on the enterprise configuration for your appみたいなエラーが出るので、以下で作成するアプリユーザーを利用しなければなりません。

アプリユーザーの作成

コマンドで作成します。
Bearerの部分はアプリ構成のDeveloperトークンで生成してください(60分で利用できなくなります)。

curl https://api.box.com/2.0/users \
     -H "Authorization: Bearer ***" \
     -d '{"name": "box-list-user", "is_platform_access_only": true}' \
     -X POST -vvv

コマンドを実行すると、以下のようなレスポンスが来るのでUserIDを取っておいてください。

{
  "type": "user",
  "id": "これを保存してください",
  "name": "box-list-user",
  "login": "AppUser_***@boxdevedition.com",
  "created_at": "2019-02-15T01:15:53-08:00",
  "modified_at": "2019-02-15T01:15:53-08:00",
  "language": "en",
  "timezone": "America/Los_Angeles",
  "space_amount": ***,
  "space_used": 0,
  "max_upload_size": ***,
  "status": "active",
  "job_title": "",
  "phone": "",
  "address": "",
  "avatar_url": "***"
}

また、[管理コンソール][ユーザーとグループ]にAppUserが追加されているはずです。

ユーザー設定で[フォルダの追加または作成]から管理したいフォルダを選んでください。
スクリーンショット 2019-02-15 18.20.40.png
以上で設定は終わりです。

boxのファイル一覧を取得するプログラムの作成

プライベートキーの切り出し

プログラム作成の前に、秘密鍵からprivateKey情報を別のファイルとして切り出します。

$ cat key.json | jq .boxAppSettings.appAuth.privateKey

適当にファイル名をつけて(box_private_keyとか)プロジェクトフォルダ内(input/配下とか)に保存しておきます。

Serverless Frameworkで作成したhandler.rbを以下のように編集します。
(単純にファイルの一覧を表示し、ファイルの数を調べるプログラムです)

require "boxr"

def run(event:, context:)
  token = Boxr::get_user_token(
  ENV.fetch('BOX_USER_ID'),
  private_key: File.open('input/box_private_key').read.gsub("\\n", "\n"),
  private_key_password: ENV.fetch('BOX_JWT_PRIVATE_KEY_PASSWORD'),
  public_key_id: ENV.fetch('BOX_JWT_PUBLIC_KEY_ID'),
  client_id: ENV.fetch('BOX_CLIENT_ID'),
  client_secret: ENV.fetch('BOX_CLIENT_SECRET'))
  client = Boxr::Client.new(token.access_token)
  cnt = show(client, '0', '')

  { statusCode: 200, num_of_files: JSON.generate(cnt) }
end

# ファイルの一覧を全て捜査する
def show(client, folder_id, parent_path)
  cnt = 0
  items = client.folder_from_id(folder_id).item_collection.entries
  items.each do |item|
    if item.type == 'folder'
      cnt += show(client, item.id, "#{parent_path}/#{item.name}")
    elsif item.type == 'file'
      cnt += 1
      p "#{parent_path}/#{item.name}"
    end
  end
  cnt
end

補足すると

  • Boxr::get_user_tokenでクライアントに接続
  • client.folder_from_id(folder_id)でフォルダ内の内容の取得
    • 0を指定するとルートフォルダからになります
    • フォルダIDはブラウザから確認できるので、指定のフォルダに対し検索もかけられます
  • item.typeでフォルダなのかコンテンツなのかを判断しています

handlerができたら、serverless.ymlを以下のようにします。
ただし、BOX_USER_IDの部分はすべてのユーザーの場合は自身のアカウントIDを、AppUserのみの場合はAppUser生成時に得られたIDを指定してください。

service: box-list

provider:
  name: aws
  runtime: ruby2.5

  stage: dev
  region: us-east-1

  environment:
    BOX_USER_ID: <user_id / app_user_id>
    BOX_JWT_PRIVATE_KEY_PASSWORD: ***
    BOX_JWT_PUBLIC_KEY_ID: ***
    BOX_CLIENT_ID: ***
    BOX_CLIENT_SECRET: ***


functions:
  box:
    handler: handler.run
    events:
      - schedule: rate(100 minutes)
    timeout: 300
  • 今回は本当に雑に100分ごとに実行していますが、好きなようにスケジュール式を変えてください。
  • environment配下に、それぞれの値を入れてください。
    • セキュリティ的にはシステムの環境変数から取得する方がいいかも。

動作確認

ここまで行ったら一旦動作確認をします。
Serverless Frameworkがイカしている部分としてローカルで簡単に試せるというところがあります。

sls invoke local -f box

これで動いているみたいならデプロイするだけ!

Tips

CSVファイルをパースする。

CSV.parse(client.download_file(item.id).encode('UTF-8', 'Shift_JIS'), headers: true)

他のファイルもclient.download_fileで制御できそうです。
詳しくは公式のAPI仕様を確認してください。
FYI: https://box-content.readme.io/reference

まとめ

マジレスするとものすごく大変でした :innocent: :innocent: :innocent:
ドキュメントが地味にわかりにくいので色々試し試しやった感じです。
でも、一回動いてくれるようになると大変便利&色々応用できそうです:thumbsup::thumbsup::thumbsup:

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

酒が飲めるワンライン

perl (5.18.2 で確認)

perl -E 'say"$_月は横浜で酒が飲めるぞ"for(1..12)'

ruby

ruby -e '12.times{|i|puts"#{i+1}月は横浜で酒が飲めるぞ"}'

bash

echo -e {1..12}月は横浜で酒が飲めるぞ"\n"

python3 (3.7.2 で確認)

python3 -c 'for n in range(1, 13): print(f"{n}月は横浜で酒が飲めるぞ")'

emacs-lisp (GNU Emacs 26.1 で確認)

(require 'cl)(let ((x 0))(while (< x 12) (cl-incf x)(insert (format "%d月は横浜で酒が飲めるぞ\n" x))))

php (PHP 5.6.30 で確認)

php -r 'for($i=1;$i<13;$i++){echo $i."月は横浜で酒が飲めるぞ\n";}'

Elixr (1.6.4 で確認)

elixir -e "1..12 |> Enum.map(&(\"#{&1}月は横浜で酒が飲めるぞ\n\")) |> IO.puts"

MySQL版 (MySQL 8 で確認)

CREATE DATABASE s;CREATE TABLE s.y (m int auto_increment, PRIMARY KEY (`m`));INSERT INTO s.y value (),(),(),(),(),(),(),(),(),(),(),();SELECT CONCAT(m, '月は横浜で酒が飲めるぞ') FROM s.y;DROP DATABASE s;

PostgreSQL (9.3 で確認)

psql -c "WITH RECURSIVE seq(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM seq WHERE i < 12) SELECT i || '月は横浜で酒が飲めるぞ' FROM seq;"

JavaScript (node.js v10.14.2)

node -e "for(let i=1;i<13;i++)console.log(i+'月は横浜で酒が飲めるぞ')"

Haskell (ghc8.4.4)

ghc -e 'mapM_ (\n-> putStrLn $ show n ++ "月は横浜で酒が飲めるぞ") [1..12]' 

GAWK(GNU Awk 4.1.3)

gawk 'BEGIN{for(i=1;i<13;i++) print i"月は横浜で酒が飲めるぞ"}'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Codewars で複数のプログラミング言語で全ての問題を解いていきたい時に便利な操作

クリックすると私が解いた問題が見れます。
# 今回は画像(スクショ)だけで手抜き~!

# Codewars についてご存じない方は、先に『【Codewars】ブラウザでコーディングの基礎からトレーニングできるサイト (ブラウザでvimが使えて32種類のプログラミング言語に対応。4000個以上の問題が投稿されています!)』 をお読み頂くようお願い致します。

image.png

image.png

image.png

image.png

このような感じで、View Profile メニューから Kata タブを選択することで、他の言語で解き忘れがないか確認できます。すでにやった問題を自分の学習中の言語全てで解いてみたい方におススメです。

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

rails で セレクトタグの生成

Railsでセレクトタグの生成

今回、ECサイトの買い物かご的な物を再現してみようと思い作成していた所、少々分かりづらかった所があったので記事にしてみました。

cart.PNG

select_tag

Rails ドキュメント
http://railsdoc.com/references/select

ここに説明が載っているものの、僕には理解が難しかった・・・

select_tagの使い方

ドキュメントには、

select_tag(要素名, タグを表す文字列 [, オプション])

の様に書かれていました。

色々試してみた所、要素名の所で指定した値がname属性として送信され、

params[:要素名]

で値を取得できるようです。

値の作成

Railsドキュメント
http://railsdoc.com/references/options_for_select

セレクトタグの値を作成する場合は、options_for_selectを使います。

  <%= select_tag("num", options_for_select((タグの配列 or ハッシュ)) %>

とすることで値を設定できます。

今回は1から100までの値を設定したので、

  <%= select_tag("num", options_for_select((1..100)) %>

この様に記述しました。

selected設定

今回は、買い物かごの作成ということだったので、購入予定の個数設定をしておかなくてはならないためselected指定をする必要がありました。

これは、options_for_selectの:selectedオプションを使用することで可能になります。

そのため、options_for_selectの第2引数に

  <%= select_tag("num", options_for_select((1..100) ,
      :selected => item.item_number) ) %>

とすることで、購入予定数をselected指定しました。

上記の様に、

:selected => 値

とすることで、指定ができます。

感想

ドキュメントを読むのは難しい・・・

言葉の定義など、理解があいまいな部分が多く、
用語を明確に覚える必要があると感じた。

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

Ruby.学習その9

投稿目的

  • 個人の学習目的で投稿していきます。
  • Rubyの入門を読みながら自分なりの思考で記述しています。

参考資料

ゼロからわかる Ruby 超入門 (かんたんIT基礎講座)

クラスの高度な話

インスタンス変数を簡略的に定義

  • 下記が簡略的ではない書き方になります。
class Greeting # Greetingクラス定義
    def morning # morningメソッド定義
        @morning # morningインスタンス変数定義
    end
    def morning=(text) # morning=メソッドに引数1つ定義
        @morning = text # 受け取った引数をインスタンス変数に代入
    end
end

greeting = Greeting.new # インスタンス作成
greeting.morning = "おはようございます" # morning=メソッドを呼び出し
puts greeting.morning # morningメソッドを呼び出し 
# 結果: おはようございます
  • 下記のように記述することでインスタンス変数を簡略的にすることが可能です。
attr_reader :インスタンス変数名

attr_readerメソッド

  • 上記のGreetingクラスに追加すると下記のようになります。
  • attr_readerメソッドは同名のインスランス変数を戻り値とするメソッドを定義します。
    • @を記述せずに:に繋げて記述します。
class Greeting
    attr_reader :morning # 戻り値が同名のインスタンス変数のメソッドを定義
    def morning=(text)
        @morning = text
    end
end

greeting = Greeting.new
greeting.morning = "おはよう"
puts greeting.morning
# 結果: おはよう

attr_writerメソッド

  • attr_writerメソッドを利用することで引数を受け取って同名のインスタンス変数に代入するメソッドを簡略的に定義することができます。
class Greeting
    attr_reader :morning 
    attr_writer :morning # 同名のインスタンス変数に引数を受け取り代入するメソッドを定義
end

greeting = Greeting.new
greeting.morning = "おはよう!!"
puts greeting.morning
# 結果: おはよう!!

attr_accessorメソッド

  • さらに上記の2つはセットで利用することが多いのでその2つの機能を持つメソッドがattr_accessorメソッドです。
class Greeting
    # 同名のインスタンス変数に代入、戻り値が同名のインスタンス変数
    attr_accessor :morning 
end

greeting = Greeting.new
greeting.morning = "Good morning"
puts greeting.morning
# 結果: Good morning

self

  • selfは自分自身(レシーバ)を指している。
    • レシーバとはメソッドを呼び出せるオブジェクト.
  • メソッド内でselfを指定すると返ってくるオブジェクトはインスタンスメソッドならクラスのインスタンス、クラスメソッドならクラスのように自分自身を指している。
インスタンスメソッド
class Greeting
    def me # インスタンスメソッドを定義
        self.morning # インスタンスメソッドなのでクラスのインスタンスを指している
    end
    def morning # インスタンスメソッドを定義
        puts "おはよう"
    end
end

greeting = Greeting.new
greeting.me # 結果: おはよう
クラスメソッド
  • クラスメソッドの場合はメソッド名の前にselfを付ける必要がある。
class Greeting
    def self.me # クラスメソッドを定義
        self.morning # クラスメソッドなのでクラス自身を指している
    end
    def self.morning #クラスメソッドを定義
        puts "おはよう"
    end
end

Greeting.me # 結果: おはよう
  • 自分自身のメソッドを呼び出すのにself(レシーバ)と付けるのは手間なのでself(レシーバ)指定を省略することができる。
class Greeting
    def me
        morning 
    end
    def morning 
        puts "おはよう"
    end
end

greeting = Greeting.new
greeting.me # 結果: おはよう

インスタンスメソッドとクラスメソッドが持つインスタンス変数は別物

  • インスタンス変数の持ち主はselfが指すオブジェクトです。
    • クラスにインスタンスとクラスはオブジェクトが違うのでインスタンス変数の持ち主も別々になります。
class Greeting
    def morning # インスタンスメソッドを定義
        @morning = "おはよう" # インスタンスメソッドのインスタンス変数に代入
    end
    def self.morning # クラスメソッドを定義
        @morning # クラスメソッドのインスタンス変数が戻り値で返る
    end
end

greeting = Greeting.new
greeting.morning # インスタンスメソッドのmorningを呼び出し
p Greeting.morning # クラスメソッドのmorningを呼び出し 
# 結果: nil

クラス変数

  • インスタンス変数、ローカル変数の他にクラス変数が存在します。
    • クラス変数は変数名の前に@@を付けることで定義できます。
    • クラス変数はクラスを継承した先でも利用することができる。
class GreetingA
    @@morning = "おはよう" # クラス変数に代入
    def morning
        @@morning 
    end
end

greetingA = GreetingA.new
puts greetingA.morning # 結果: おはよう

class GreetingB < GreetingA # GreetingAのクラスをGreetingBに継承
    def morning
        @@morning # greetingAのクラス変数を利用
    end
end

greetingB = GreetingB.new
puts greetingB.morning # 結果: おはよう

文字列を調べる正規表現

文字列を含むかを判定

  • match?メソッドを利用することで特定の文字列を含んでいるかが判定できます。
  • match?の引数にパターンを渡すことで含むかを判定できます。
"文字列".match?(/正規表現パターン/)
  • /で囲むことで正規表現(Regexp)オブジェクトとなりこれをパターンと呼びます。
/正規表現パターン/
  • 下記はフルーツと文字が含むものがtrueとなります。
p "グレープフルーツ".match?(/フルーツ/) # true
p "りんご".match?(/フルーツ/) # false
p "ドラゴンフルーツ".match?(/フルーツ/) # true

その他の正規表現

  • \zを加えることで末尾に指定された文字が含まれているかを判定します。
  • \Aを加えることで先頭に指定された文字が含まれているかを判定します。
p "グレープフルーツ".match?(/フルーツ\z/) # true
p "フルーツレモン".match?(/\Aフルーツ/) # true
  • よく使われる正規表現.
  • []で囲むと中の1文字どれかが含まれているかを判定します。
/[abc]/ # aかbかcが含まれていればtrue
  • []の中に範囲指定で記述して判定することもできます。
  • アルファベットの大文字か小文字、数字のいずれか1文字が含まれているかを判定します。
p "りんごv".match?(/[A-Za-z0-9]/) # true
p "りんご5".match?(/[A-Za-z0-9]/) # true
  • .は任意の1文字。
p "a0b".match?(/a.b/) # true
p "agb".match?(/a.b/) # true
  • *は前の文字が0回以上繰り返すかどうかを判定します。
p "abc".match?(/ab*c/) # true
p "c".match?(/ab*c/) # false
p "aaac".match?(/ab*c/) # true
  • +は前の文字が1回以上繰り返すかを判定します。
p "ac".match?(/ab+c/) # false
p "abc".match?(/ab+c/) # true
p "bbc".match?(/ab+c/) # false

条件と一致する文字を置換

  • gsubメソッドを利用することで文字の置換をすることができます。
  • 第1引数が置換元で第2引数が置換先になります。
p "フルーツオレ".gsub("フルーツ", "カフェ") # カフェオレ
p "グレープフルーツサワー".gsub("サワー", "ジュース") # グレープフルーツジュース
p "フルーツフルーツ".gsub(/\Aフルーツ/, "ミックス") # フルーツミックス

正規表現とif文

  • 正規表現を利用してif文で分岐することができます。
  • 下記のプログラムはフルーツと含まれているものだけを表示する条件です。
["バナナジュース", "グレープフルーツサワー", "フルーツオレ"].each do |drink|
    puts drink if drink.match?(/フルーツ/) 
end
# 結果: グレープフルーツサワー フルーツオレ 

ブロックの高度な話

  • eachメソッドなどを使用する際にdoからendをブロックと呼びます。
    • ブロックとはプログラムのかたまりをメソッドへと渡すことができる仕組みです。
    • ブロックは引数と似ています引数はオブジェクトを渡してブロックは処理のかたまりを渡すイメージです。
    • ブロックは1つしか渡すことができません。

渡されたブロックを実行

  • block_given?メソッドを利用することでブロックが渡されたどうかを判別してくれます。
def foo
    p block_given
end

foo #=> false

foo do
end #=> true
  • 渡されたブロックを実行するにはyieldを利用します。
def dice
    if block_given? # ブロックの有無を判別
        puts "run block" 
        yield # 渡されたブロックを実行
    else    # ブロックが渡されていない場合の処理
        puts "normal dice" 
        puts [1, 2, 3 , 4, 5, 6].sample 
    end
end

dice # 結果: ブロックを渡していないので1~6をランラムで表示

dice do # ブロックが渡されたのでブロックの処理を実行
    puts [4, 5, 6].sample # 結果: 4~6をランダムに表示
end

渡されたブロックを引数で受け取る

  • 引数にブロックを渡す際は変数名の前に&を付けることで受け取ることができます。
  • callメソッドで変数に代入されたブロックを実行することができる。
  • 変数に代入されているブロックをprocオブジェクトと呼びます、プログラムの処理もオブジェクトとして扱うことができます。
def foo(&greeting) # &でブロックを受け取る
    greeting.call #  代入されたブロックを実行
end

foo do
    puts "おはようございます!" 
end
# 結果: おはようございます!

ゼロからわかるRuby超入門の教本は終了です。

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

【Ruby】シーザー暗号作ってみた

Rubyで簡易なシーザー暗号プログラムを作りました。
初投稿で、至らぬ点も多いですが、よろしくお願いします。

シーザー暗号とは?

平文の各文字を辞書順に3文字のみシフトし、暗号文をつくる暗号のこと。
単一換字式暗号の一種で、カエサル暗号とも呼ばれている。

要はアルファベット順に文字をシフトさせて、暗号化するってことですね。
例: enter → bkqbo

実現するには?

コードを実現するにあたって、考えたことは以下になります。

  1. 文字列って、数字で表現できるのでは?
  2. 文字列 → 数値 → シフト後の数値 → 文字列と変換すれば可能?
  3. 文字列が長くても対応できるように!

上記の3点を意識して、コードを書いてみました。

実際に書いたコード

code.rb
puts "キーワードを入力してください(アルファベット小文字のみ)"
keyword = gets.chomp

puts "暗号化前: #{keyword}"

# 暗号化した文字列を格納するための配列を定義
code = []

# keyword.charsで入力された文字列を1文字ずつ分解して配列へ
keyword.chars.each do |char|
  num = (char.ord - 3) # .ord で文字コードへ変換(数値化)し、3つ分、シフトする
  code << num.chr # 文字コードを文字列へ戻し、配列codeに入れていく
end

# 配列codeに入った文字列を全て連結させて出力
puts "暗号化後: #{code.join("")}"

ターミナルでの出力

キーワードを入力してください(アルファベット小文字のみ)
desk
暗号化前: desk
暗号化後: abph

やった!できた! 他のも試してみよう・・・

キーワードを入力してください(アルファベット小文字のみ)
apple
暗号化前: apple
暗号化後: ^mmib

aが記号に変換されてしまった・・・

調べてみると、
aの文字コード: 97 → シフト後: 94 となり、文字コード: 94は「^」みたいです...

となると、「a ~ z」の文字コードでの範囲は「97 ~ 122」。この範囲外では、アルファベットの小文字ではなくなってしまう。
abcやxyzにも対応できるようにしなければ...
そこでコードを以下のように改良しました。

改良版

code.rb
puts "パスワードを入力してください(aからzまでの小文字)"
keyword = gets.chomp

puts "暗号化前: #{keyword}"
code = []

keyword.chars.each do |char|
  num = char.ord - 97
  num2 = (num - 3) % 26
  num3 = num2 + 97
  code << num3.chr
end

puts "暗号化後: #{code.join("")}"

each文の中の処理を少し変更してみました!

each内部の処理ですが...
d を例として説明すると、
dの文字コードは「100」のため、numに入る値は「3」になります。
次にnumに対して、「3」(シフトさせる分の数値)を引き、 %26 してあげると num2は「0」です。
その後、num2に「97」を再び足すことでnum3が「97」となります。
最後に、このnum3に 「.chr」することで文字列に戻り、「a」となります。

num2の計算ですが、%26してあげることで、計算結果は「0 ~ 25」のどれかになります。
たとえ、num2が負の値であってもです。

これは割り算の余りの基本式に当てはめて考えてみると分かるので、気になる方はやってみてください。

(割られる数)=(割る数)×(商)+(余り)

最後に書き換えたコードをターミナルで実行してみましょう!

パスワードを入力してください(aからzまでの小文字)
apple
暗号化前: apple
暗号化後: xmmib

アルファベットの小文字限定ですが、無事暗号化することができました。
まだ大文字や数字には対応していないので、時間があればやってみようかと思います。

分かりづらかったかもしれませんが、ここまで読んでいただきありがとうございました。

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

CodinGame の TRON BATTLE で FloodFill アルゴリズムを実装してみた

# この記事は、『CodinGame はBOT(AIプログラム)でバトルするのが正しい楽しみ方かもしれません』 の続きです。

以下のようなデバッグ出力ができるように CodinGame の TRON BATTLE プログラムを改造してみた。

  • P=0 という出力から、自機のプレイヤー番号は0であることが分かる。
  • Input{X0:2, Y0:2, X1:10, Y1:0} という出力からプレイヤー0のスタート位置(tail)は、(2, 2) であることと、現在位置(head)の座標が (10, 0) であることが分かる。 *自機のプレイヤー(プレイヤー0)のヘッドから見て現時点で到達可能であるマスが+記号で表示されている。ちなみにマイナス記号は何もない印である。
P=0
Input{X0:2, Y0:2, X1:10, Y1:0}
+ + + + + + + + + + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

1. 出来たソース

using System;
using System.Linq;
using System.IO;
using System.Text;
using System.Collections;
using System.Collections.Generic;

class Player
{
    static void Main(string[] args) {
        Player control = new Player();
        string[] numbers;
        Input[] inputs;
        // game loop
        while (true) {
            numbers = Console.ReadLine().Split(' ');
            int N = int.Parse(numbers[0]);
            int P = int.Parse(numbers[1]);
            Console.Error.WriteLine("P={0}", P);
            inputs = new Input[N];
            for (int i = 0; i < N; i++) {
                numbers = Console.ReadLine().Split(' ');
                int X0 = int.Parse(numbers[0]);
                int Y0 = int.Parse(numbers[1]);
                int X1 = int.Parse(numbers[2]);
                int Y1 = int.Parse(numbers[3]);
                inputs[i] = new Input(i, X0, Y0, X1, Y1);
            }
            Input me = inputs[P];
            string dir = control.HandleInputs(me, inputs);
            control.DumpMap();
            Console.WriteLine(dir);
        }
    }
    Cell[,] map = new Cell[30, 20];
    Queue<Cell> ffQueue = new Queue<Cell>();
    private Player() {
        for(int y=0; y<20; y++) {
            for(int x=0; x<30; x++) {
                map[x, y] = new Cell(x, y, -1);
            }
        }
    }
    private string HandleInputs(Input me, Input[] inputs) {
        Console.Error.WriteLine(me);
        foreach(var input in inputs) {
            if (input.X1 < 0) DeleteIdsFromMap(input.Id);
            else AddInputToMap(input);
        }
        ExecuteFloodfill(me);
        if (CanMoveTo(me, -1, 0)) return "LEFT";
        if (CanMoveTo(me, 1, 0)) return "RIGHT";
        if (CanMoveTo(me, 0, -1)) return "UP";
        if (CanMoveTo(me, 0, 1)) return "DOWN";
        return "?";
    }
    void AddInputToMap(Input input) {
        this.map[input.X1, input.Y1].V = input.Id;
    }
    void DeleteIdsFromMap(int id) {
        for(int y=0; y<20; y++) {
            for(int x=0; x<30; x++) {
                if (map[x, y].V == id) map[x, y].V = -1;
            }
        }
    }
    bool CanMoveTo(Input me, int xOffset, int yOffset) {
        int x = me.X1+xOffset;
        int y = me.Y1+yOffset;
        if (x < 0) return false;
        if (x > 29) return false;
        if (y < 0) return false;
        if (y > 19) return false;
        return map[x, y].V == -1 || map[x, y].V == 9;
    }
    bool IsEmptyCell(Cell center, int xOffset, int yOffset) {
        int x = center.X+xOffset;
        int y = center.Y+yOffset;
        if (x < 0) return false;
        if (x > 29) return false;
        if (y < 0) return false;
        if (y > 19) return false;
        return map[x, y].V == -1;
    }
    void DumpMap() {
        for(int y=0; y<20; y++) {
            for(int x=0; x<30; x++) {
                if (map[x, y].V == -1) Console.Error.Write("-");
                else if (map[x, y].V == 9) Console.Error.Write("+");
                else Console.Error.Write(map[x, y].V);
                Console.Error.Write(" ");
            }
            Console.Error.WriteLine();
        }
    }
    void ExecuteFloodfill(Input me) {
        for(int y=0; y<20; y++) {
            for(int x=0; x<30; x++) {
                if (map[x, y].V == 9) map[x, y].V = -1;
            }
        }
        Cell c = map[me.X1, me.Y1];
        if (IsEmptyCell(c, -1, 0)) ffQueue.Enqueue(map[c.X-1, c.Y]);
        if (IsEmptyCell(c, 1, 0)) ffQueue.Enqueue(map[c.X+1, c.Y]);
        if (IsEmptyCell(c, 0, -1)) ffQueue.Enqueue(map[c.X, c.Y-1]);
        if (IsEmptyCell(c, 0, 1)) ffQueue.Enqueue(map[c.X, c.Y+1]);
        FloodfillLoop();
    }
    void FloodfillLoop() {
        while(ffQueue.Count > 0) {
            Cell c = ffQueue.Dequeue();
            if(map[c.X, c.Y].V != -1) continue;
            c.V = 9;
            if (IsEmptyCell(c, -1, 0)) ffQueue.Enqueue(map[c.X-1, c.Y]);
            if (IsEmptyCell(c, 1, 0)) ffQueue.Enqueue(map[c.X+1, c.Y]);
            if (IsEmptyCell(c, 0, -1)) ffQueue.Enqueue(map[c.X, c.Y-1]);
            if (IsEmptyCell(c, 0, 1)) ffQueue.Enqueue(map[c.X, c.Y+1]);
        }
    }
}
class Input
{
    public int Id;
    public int X0;
    public int Y0;
    public int X1;
    public int Y1;
    public Input(int id, int x0, int y0, int x1, int y1) {
        this.Id = id;
        this.X0 = x0;
        this.Y0 = y0;
        this.X1 = x1;
        this.Y1 = y1;
    }
    public override string ToString() {
        return String.Format("Input{{X0:{0}, Y0:{1}, X1:{2}, Y1:{3}}}", X0, Y0, X1, Y1);
    }
}
class Cell
{
    public int X;
    public int Y;
    public int V;
    public Cell(int x, int y, int v) {
        this.X = x;
        this.Y = y;
        this.V = v;
    }
    public override string ToString() {
        return String.Format("Cell{{X:{0}, Y:{1}, V:{2}}}", X, Y, V);
    }
}

2. 最後に

TRON BATTLE については書く記事としてはこれで終わりになるかもしれません。
C# で挑戦される方の「手始め」の参考にでもなればと思い投稿してみました。

# Wood 1 League を抜け出したいのだが…

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

メモ:RailsのActionMailerでテキストメールを自動生成する

この記事は

  • 技術メモです

やりたかったこと

  • RailsのActionMailerを使ってメールを送信しているサービスがあります
  • そこですでにHTMLメールを送信しているのですが、一部メーラでHTMLメールは受信できないという話がありました
  • そこで全メールについてTEXTとのマルチパートメールで送出する必要がでてきました

普通にやると

  • 既存のHTMLメールのテンプレートに加えて、TEXTメールのビューテンプレートも作成するとマルチパートで送信することが可能です
  • しかし、TEXT/HTMLのビューテンプレートを両方メンテナンスしていくのは結構きついです

actionmailer-text gem

  • actionmailer-textを使うと、自動的にHTMLメールからタグを除去していい感じにテキストメールを送出してくれます
  • こんな感じっぽく動きます
    • <br/>は改行に置換
    • リンクはリンク文字列(URL)みたいな感じに置換
    • そのほかの普通のタグは除去
    • このへんを見るとなんか一生懸命正規表現で置換してるっぽい

使い方

  • gemを入れます
Gemfile
gem 'actionmailer-text'
  • application_mailerに所定のモジュールをincludeします
application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  # 追記
  include ActionMailer::Text
end
  • ちなみにdeviseを使っている場合、deviseから送られるメールはApplicationMailerを継承していないことがあるので、devise設定ファイルで指定します
config/initializers/devise.rb
config.parent_mailer = 'ApplicationMailer'
  • 以上で普通に全てのメールがTEXTとHTMLのマルチパートメールになって送信されます

終わりに

  • ガラケーとかのテキストしか受けられないメーラ爆発しろ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[rails] モデルに存在しない値をjsonフォーマットで返却する方法

概要

モデルに存在しない値をjsonフォーマットで返却したい場合どうするのか。
答えは、アクション関数のrender jsonする際にto_jsonを使いmethodsのオプションにモデルの任意の関数を指定してあげればいいです。

言葉では伝えづらいので、、実際にやってみた結果を御覧ください。

改修前

Reportのスキーマがどうなっているかは説明を省きますが、
コントローラーで以下のようなアクションの実装をしていて、reports.jsonにGETリクエストするとモデルの内容をjsonで出力してくれます。

reports_controller.rb
  # GET /reports
  # GET /reports.json
  def index
    @reports = Report.all

    respond_to do |format|
      format.html { render :index }
      format.json { render json: @reports, status: :ok }
    end
  end
reports.json
[
  {
    "id": 4,
    "text": "hoge",
    "created_at": "2019-02-13T11:16:23.000+09:00",
    "updated_at": "2019-02-13T11:16:23.000+09:00",
    "deleted_at": null
  }
]

改修後

では、実際にどう実装するかをやってみます。
まずは、モデル側に任意の関数を実装してください。以下の関数ではcreated_atの値を日本人がわかりやすい日時の文字列フォーマットに変換しています。

report.rb
class Report < ApplicationRecord
  # 省略

  def created_at_formatted
    self.created_at.strftime("%Y年%-m月%-d日")
  end
end

次に、アクションの実装のrender jsonしてあげている箇所を以下のように変更してあげます。

reports_controller.rb
  # GET /reports
  # GET /reports.json
  def index
    @reports = Report.all

    respond_to do |format|
      format.html { render :index }
      format.json { render json: @reports.to_json(methods: :created_at_formatted), status: :ok }
    end
  end
-      format.json { render json: @reports, status: :ok }
+      format.json { render json: @reports.to_json(methods: :created_at_formatted), status: :ok }

そうしますと以下のようにモデルには存在しない値をjsonで返却することができました。

reports.json
[
  {
    "id": 4,
    "text": "hoge",
    "created_at": "2019-02-13T11:16:23.000+09:00",
    "updated_at": "2019-02-13T11:16:23.000+09:00",
    "deleted_at": null,
    "created_at_formatted": "2019年2月13日"
  }
]

以上になります。

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

Rails × Mountain View(マウンテンビュー)で作るCSSスタイルガイド&コンポーネント

はじめまして。

Mountain View導入の経緯

普段 Ruby On Railsでプロダクトを開発しており、gemで完結するCSSのスタイルガイドを探していました。

有名なgemをいくつか素振りしてみたのですが、LivingStyleGuideはスタイルガイドを簡単にカテゴリ分けできず、Hologramはメンテナンスコストが高そうに感じました。

そんな時に、Railsのビューコンポーネントをそのままスタイルガイド化できるMountainViewと言うgemを知りました。

Mountain Viewとは

https://github.com/devnacho/mountain_view

With Mountain View you create reusable components for your Rails frontend, while generating a living style guide.

image.png

Mountain Viewを使用すると、スタイルガイドを生成しつつ、Railsフロントエンド用の再利用可能なコンポーネントを作ることができます。

この記事で紹介すること

紹介しておいてなんですが、私はパフォーマンス上の理由からMountain Viewのコンポーネント機能をそれほど使っていません。
(コンポーネントはRailsのパーシャルに任せれば済む話で、どちらかと言うとスタイルガイドとしての機能が欲しい...)

この記事では私がMountain Viewを導入し、コンポーネントとして使用することを諦め、スタイルガイドとして使用することに落ち着くまでに試行錯誤した内容をまとめています。

javascriptを使ったモダンなフロントエンド開発からは遠い話になりますのでご容赦ください。

導入 ~ スタイルガイドの作成

Rails 5.2.1環境で公式のREADMEだけを参考に導入することができました。

まずは Gemfilemountain_view を追加します。

# コンポーネント機能を利用する場合はグローバルに読み込みます
gem 'mountain_view'

bundle installを実行し、routes.rb ファイルに次の行を追加します。

mount MountainView::Engine => "/mountain_view" if Rails.env.development?

下記のコマンドでMountain Viewの新しいコンポーネントを作成します。

rails generate mountain_view:component button

すると、次のようなディレクトリとファイルが作成されます。

app/
  components/
    button/
      _button.html.erb
      button.css
      button.js
      button.yml

拡張子にscssやslimなど、Railsで使っているプリプロセッサを使うことができます。

rails s でサーバーを立ち上げてみましょう。
ローカル環境からbuttonコンポーネントのスタイルガイドのページが作成されていることが確認できると思います。
http://localhost:3000/mountain_view

i18n対応

i18n対応で日本語化(default_localeをjaに変更)していた場合、Mountain Viewにja.ymlが存在しないためtitleタグの表示で下記のようなエラー出ているかもしれません。

<title>&lt;span class="translation_missing" title="translation missing: ja.mountain_view.layout.styleguide_title"&gt;Styleguide Title&lt;/span&gt;</title>

私はこちらの en.ymlファイルの内容をコピペした config/locales/mountain_view.ja.yml のようなファイルを作成しました。

buttonコンポーネントを作ってみる

ここでは bootstrap を使用してコンポーネント作りを試してみます。
Gemfile に以下を追加して bundle install します。

gem 'bootstrap'

buttonコンポーネントのscssでbootstrapを読み込みます。

components/button/button.scss
@import "bootstrap";

bootstrapのbuttonコンポーネントをMountainViewに反映させた例です。

app/components/button/_button.html.erb
<button type="button" class="btn <%= properties[:modifier] %>">
  <%= properties[:title] %>
</button>
app/components/button/button.yml
-
  :modifier: btn-primary
-
  :modifier: btn-secondary
-
  :modifier: btn-success
-
  :modifier: btn-danger
-
  :modifier: btn-warning
-
  :modifier: btn-info
-
  :modifier: btn-light
-
  :modifier: btn-dark

image.png

キャッシュの問題

Mountain ViewではCSSの更新やコンポーネントの作成を行った後にキャッシュが残ってしまい、下記のコマンドを実行しないとコンポーネントのデザインがうまく反映されないことがありました。

bin/rake tmp:cache:clear

buttonコンポーネントを使ってみる

定義したコンポーネントはrender_component メソッドを使用することで使うことができます。
第一引数にコンポーネント名、第二引数にハッシュ値を渡して使います。

<%= render_component("button", { title: "Btn Primary", modifier: "btn-primary" }) %>

コンポーネントにブロックを渡し、properties[:yield]で読み込んで使用することもできます。

app/components/button/_button.html.erb
<button type="button" class="btn <%= properties[:modifier] %>">
  <%= properties[:yield] %>
</button>
使い方.html.erb
<%= render_component("button", {modifier: "btn-primary" }) do %>
  Btn Primary
<% end %>

プレゼンターを使ってみる

コンポーネントの階層に{コンポーネント名}_component.rbファイルを追加してMountainViewコンポーネント用のプレゼンターを定義することができます。

app/
  components/
    button/
      _button.html.erb
      button.css
      button.js
      button.yml
      + button_component.rb

MountainView::Presenterを継承して使います。
プロパティのデフォルト値なども使用できます。

app/components/button/button_component.rb
class ButtonComponent < MountainView::Presenter
  properties :modifier, :title
  property :element, default: 'btn'

  def modifier_title
    title || properties[:modifier].titleize
  end
end

定義したメソッドやプロパティをコンポーネントのパーシャルで利用することができます。

app/components/button/_button.html.erb
<button type="button" class="<%= element %> <%= modifier %>">
  <%= title %>
</button>

Railsのコンポーネント管理がMountainViewで完結して最高!
...と思ったのですが、このコンポーネント機能には、後述するパフォーマンス上の問題があるようです。

MountainViewのボトルネック

MountainViewのrender_component機能をeachすると、パフォーマンスがとても残念なことになってしまいます。
これを避けるために公式READMEではMountainViewのプレゼンターでrenderメソッドをオーバーライドする方法を紹介しています。

しかし、個人的にはそれよりも素直にRailsのパーシャルを使用した方がメリットが大きいなのではないかと思います。特にrender_component機能ではパーシャルのコレクション機能が使えないのが痛いので、私はeachする要素でMountain Viewのrender_component機能は使いません。

例えば、Mountain Viewを

app/components/list_item/_list_item.html.erb
<%= properties[:list_item].title %>

こんな風に定義するよりも、Railsのパーシャルで

app/views/components/_list_item.html.erb
<%= list_item.title %>

このように定義しておけば、renderをキャッシュしてくれますし、collection機能でn+1対策もできます。

使い方.html.erb
<% @lists = List.all %>

# @listsが100個あったら100回render
<% @lists.each do |list_item| %> 
  <%= render_component("list_item", {list_item: list_item}) %>
<% end %>

# @listsの結果が100個あっても1回のrenderで済む
<%= render partial: 'components/list_item', collection: @lists, as: :list_item %>

最高のビューコンポーネントはRailsのパーシャルでした...?
と、言うわけで私はMountain ViewをRailsコンポーネント管理用のスタイルガイドとして割り切って使うことにしました。

MountainViewをスタイルガイドとして使用する

MountainViewはデフォルトではスタイルガイドのHTMLタグが表示されません。
デフォルトで prism.js が使われているので、views/mountain_view/styleguide/show.html.erbをオーバーライドして、render_componentの記述の上部に下記のコードを追記しただけでスタイルガイドっぽくなってくれます。

app/views/mountain_view/styleguide/show.html.erb
...
<div class="mv-component__description__properties" style="margin-bottom: 20px;">
  <code class="language-html"><%= CGI::pretty("#{render_component(@component.name, component_stub.properties.clone)}") %></code>
</div>
...

image.png

また、MountainViewのサイドメニューがレスポンシブ表現に邪魔なので、メディアクエリで非表示にしてみました。

image.png

app/assets/stylesheets/mountain_view/layout.scss
.mv-main {
  @media screen and (max-width: 768px) {
    width: 100%;
    padding: 30px 0;
  }
}

.mv-sidebar {
  @media screen and (max-width: 768px) {
    display: none;
  }
}

イニシャライザでMountain Viewでグローバルに読み込みたいCSSを指定することができます。

config/initializers/mountain_view.rb
MountainView.configure do |config|
  config.included_stylesheets = ["mountain_view/layout"]
end

image.png

CSS読み込みの問題

MountainViewはディレクトリ名のコンポーネントをmountain_view.css.erbでディレクトリ名と同名のcssをまとめて読み込んでいるようです。

例えばbuttonコンポーネントでbootstrapをimportした後、別のコンポーネントを作ろうとしてみたところ、button.scssでしかimportしていないbootstrapが既に読み込まれています。

MountainViewの動作がCSS設計の方針と違った場合、私はマニュフェストファイルレイアウトファイルをオーバーライドして調整するようにしています。

プレゼンターをスタブとして使ってしまう

Tipsとして、私は既存のコードをコンポーネント化する際、一旦Mountain Viewのプレゼンターをスタブとして一旦置いてみる方法をとっています。

app/components/post/post_component.rb
class PostComponent < MountainView::Presenter
  # コンポーネント作りに必要なデータを取得
  def current_user
    User.find_by(email: 'necessary-user@exapmle.com')
  end
end

こんな感じでプレゼンターのメソッドを使うと既存のビューをコンポーネントでリプレースする工程がスムーズにいって便利です。

最後に

Railsのgemで完結するスタイルガイドを探したところ、Mountain Viewを改造しながら使っていくと言う道に辿り着きました。
もっとオススメのgemや、Mountain Viewの便利な使い方をご存知の方がいらっしゃいましたらぜひコメント欄で教えてください...?

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

Symbolオブジェクト

Symbolオブジェクト

文字列の前にをつけたもの。

:name

Symbolオブジェクトにすると、文字列ではなく整数として扱われる。
⇒処理速度が早くなる。

*Symbolでは同じSymbol名であれば同じオブジェクトとなる

*以下、ターミナルでpryを起動して実行
symbol1 = :name
symbol2 = :name

*以下2つは、同じIDが出力され、同一のオブジェクトであると分かる
symbol1.object_id
symbol2.object_id
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オブジェクト指向の強みは変更可能性にこそあると思った。

オブジェクト指向って再利用できるから良いってどこの記事、本にもまとめられています。
そんなの誰でもわかってるけど、それだけではいまいちピンと来ていなかった。

たぶんそれは僕がオブジェクト指向ネイティブだから。
オブジェクト指向が普及していなかった時にプログラミングをしていた人からするとオブジェクト指向は画期的だったのだろう。でもそんなことは知ったこっちゃない、そうしろとプログラミング言語に言われるのだから。誰かに「あーしろ」「こーしろ」と言われるわけではない。そうしないと基本的に動かなかったり、意味不明な挙動をするのだ。

無意識にできていることは素晴らしい事ではあるが、なぜそれが良いのか、どうしてそうするのかを知っていないとエンジニアとは言えない。

そんなある日クライアントにデモを見せるタイミングがあって、デモ中にこんな風にできない?あんな風だといいよね?みたいな言葉が出る。

そんなときに

「あー、それなら秒でできますよ。... リロードしてください。」

「おー!!」

みたいな体験があるとかっこいい。

オブジェクト指向で書いていると挙動の簡単な変更はすぐにできる。
しかもオプションでいろいろ変更できるようにしているとなおさら。
Javascriptのプラグインなどは基本的にoptionで簡単な挙動は変更できる。
クライアントは基本的に表面の動きを見ているので、表面の変えたいところが目の前で変わるのを見れば魔法か何かだと思ってしまうのも無理がない。

エンジニアにとっては当たり前であるが、クライアント(非エンジニア)にとっては魔法に見えることが多くある。その芸を支えるのがオブジェクト指向だと思った。

単に書けるエンジニアではなく、デモで魅せられるエンジニアは強いとおもった。

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

Wercker × parallel_tests

why

Wercker上でのspecテスト完遂に35分ほど要するが、その時間を短縮して開発効率を改善したい。

※Wercker:Oracle提供のCIツール

what

テストを並列実行する。

how

環境
Ruby:2.2.3
Rails:5.0.0.1
RSpec:3.5.2
sqlite:1.3.13 

local開発環境での実行

テスト並列実行用のgem "parallel_tests" を導入。

gemfile に parallel_test を追加。

Gemfile
 group :development, :test do
...
  gem 'parallel_tests'
  ...
end

gemパッケージをインストール。

$ bundle install

config/database.yml に追加

config/database.yml
test:
  database: yourproject_test<%= ENV['TEST_ENV_NUMBER'] %>

テスト用DB:yourproject_testの複製

config/database.yml の設定に応じてDBを作成。

$ bundle exec rake parallel:create RAILS_ENV=test

schemaコピー

$ bundle exec rake parallel:prepare[4]

rpsec_test実行

$ bundle exec rake parallel:spec[4]

[]は並列実行させるプロセス数

Wercker環境での実行

Wercker の docker内で複数DB作成

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

[WIP] Wercker × parallel_tests

why

Wercker上でのspecテスト完遂に35分ほど要するが、その時間を短縮して開発効率を改善したい。

※Wercker:Oracle提供のCIツール

what

テストを並列実行する。

how

環境
Ruby:2.2.3
Rails:5.0.0.1
RSpec:3.5.2
MYSQL2:0.4.5 

local開発環境での実行

テスト並列実行用のgem "parallel_tests" を導入。

gemfile に parallel_test を追加。

Gemfile
group :development, :test do
  gem 'parallel_tests'
end

gemパッケージをインストール。

$ bundle install

test用DB名に テスト環境変数 を追加

config/database.yml
test:
  database: yourproject_test<%= ENV['TEST_ENV_NUMBER'] %>

テスト用DB:yourproject_testの複製

config/database.yml の設定に応じてDBを作成。

$ bundle exec rake parallel:create RAILS_ENV=test

schemaコピー

$ bundle exec rake parallel:prepare[4]

rspec_test実行

$ bundle exec rake parallel:spec[4]

[]は並列実行させるプロセス数

Wercker環境での実行

WerckerのdockerコンテナにDB作成

wercker.yml
services:
  - id: mysql:<version>
    env:
      MYSQL_USER: *** # DBのuser名
      MYSQL_PASSWORD: *** # DBのpassword
      MYSQL_DATABASE: *** # DBの名前

後の rails-database-yml コマンドは services/env プロパティで指定した内容の database.yml (DB名、ユーザー名、パスワードのDB)を作成。

build:
  steps:
    - rails-database-yml

Werckerの同一dockerコンテナでDB複製

Initializing a fresh instance
When a container is started for the first time, a new database with the specified name will be created and initialized with the provided configuration variables. Furthermore, it will execute files with extensions .sh, .sql and .sql.gz that are found in /docker-entrypoint-initdb.d. Files will be executed in alphabetical order. You can easily populate your mysql services by mounting a SQL dump into that directory and provide custom images with contributed data. SQL files will be imported by default to the database specified by the MYSQL_DATABASE variable.

コンテナーが初めて開始されると、指定された名前の新しいデータベースが作成され、提供されている構成変数で初期化されます。 さらに、/ docker-entrypoint-initdb.dにある拡張子.sh、.sql、および.sql.gzのファイルを実行します。 ファイルはアルファベット順に実行されます。

というわけで、 /docker-entrypoint-initdb.d にSQLファイルを置いたりすればいい感じにできます。
が、SQLファイルでは、 MYSQL_DATABASE に指定したDBに対してダンプファイルを流すだけですので、他のDBを作ったりすることはできません。

wercker.yml
services:
  - id: mysql:*.*.**
    volumes:
      - "./mysql:/var/lib/mysql"
      - "./mysql/init:/docker-entrypoint-initdb.d"
    env:
      MYSQL_ROOT_PASSWORD: ***
      MYSQL_USER: ***
      MYSQL_PASSWORD: ***
      MYSQL_DATABASE: ***
file_organization.
+ docker-compose.yml
+- mysql
    +- init
        + 1_create_db.sql
        + 2_import.sh
        + dump1.gz
        + dump2.gz

参考

  1. docker-composeでmysql使うとき初回起動時に複数のDBを作る方法
  2. MySQL Docker Official Images
  3. wercker/docker-compose support? #63
  4. DockerでMySQL/PostgreSQLの初期データが生成されないときに確認すること
  5. github/docker-library/mysql/5.5/docker-entrypoint.sh
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue on RailsでActive Storageを使って画像を保存する

はじめに

Rails API モードと Vue.js で作成した 自作ブログ で Active Storage を使う際に、画像の受け渡しでハマったので実装方法を残します。
実装するのは Active Storage を使って、eyecatch (アイキャッチ画像) 付きの Post (記事) を投稿できるようなサンプルです。

環境

$ ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin16]

$ bundle exec rails --version
Rails 5.2.2

実装方法

API 部分 (Rails)

Ⅰ. サンプルアプリケーション新規作成

Rails API モードで作成します。

$ bundle exec rails new sampleapp --database=mysql --api --force
$ cd sampleapp/
$ bundle exec rails db:create

Ⅱ. Active Storage インストール

Active Storage をインストールします。

$ bundle exec rails active_storage:install
$ bundle exec rails db:migrate

Ⅲ. Post リソースの作成

今回利用する Post モデルとコントローラを作成します。

$ bundle exec rails g resource post title body:text
$ bundle exec rails db:migrate

Ⅳ. 各種ファイルの修正

各 Post に eyecatch を設定できるようにモデルを修正します。
追加する eyecatch= メソッドで、Active Storage に画像を保存します。

このメソッドは、Base64 形式で受け取った image データをエンコードし、一時的に /tmp 配下に画像ファイルを作成、作成した画像ファイルをアタッチ、その後画像ファイルを削除します。

app/models/post.rb
class Post < ApplicationRecord
  has_one_attached :eyecatch
  attr_accessor :image

  def eyecatch=(image)
    if image.present?
      prefix = image[/(image|application)(\/.*)(?=\;)/]
      type = prefix.sub(/(image|application)(\/)/, '')
      data = Base64.decode64(image.sub(/data:#{prefix};base64,/, ''))
      filename = "#{Time.zone.now.strftime('%Y%m%d%H%M%S%L')}.#{type}"
      File.open("#{Rails.root}/tmp/#{filename}", 'wb') do |f|
        f.write(data)
      end
      eyecatch.detach if eyecatch.attached?
      eyecatch.attach(io: File.open("#{Rails.root}/tmp/#{filename}"), filename: filename)
      FileUtils.rm("#{Rails.root}/tmp/#{filename}")
    end
  end
end

モデルで追加した eyecatch= メソッドに POST で受け取る画像のパラメータを渡すように修正します。

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def create
    post = Post.new(post_params)

    if post.save
      post.eyecatch = post_params[:image]
      render json: post, status: :created
    else
      render json: post.errors, status: :unprocessable_entity
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :body, :image)
  end
end

Ⅴ. 動作確認

アプリケーションを起動し、curl コマンドでアイキャッチ付き Post が作成できるか確認します。

$ bundle exec rails s

$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"post": {"title": "Sample title.", "body": "Sample body.", "image": "data:application/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAYAAACALL/6AAAACXBIWXMAADXU\nAAA11AFeZeUIAAAAgUlEQVQYlZWQMQ6DMAxFnyMGJFZCc4be/yisRYyVmDN0\niTuQULCCoH9z/PLtb/hTsi8SaA1yO85ZWHxgAqb8HuVoJAX+iKPtB17L++D+\nyN6drpOa0mj7AYBg1pmz99NmSKDiVzzmKTM/uOTYMjgQNetYuKoEqj7oCHp2\nteqn2/CVvuDZJy0n3DrVAAAAAElFTkSuQmCC\n"}}' http://localhost:3000/posts

curl コマンドで作成後、Rails Console でアイキャッチが正常に追加できているか確認します。

$ bundle exec rails c
irb(main):001:0> Post.find_by(title: "Sample title.").eyecatch.attached?
=> true

# 画像のパスは以下のように取得できます。
# irb(main):002:0> app.url_for(Post.find_by(title: "Sample title.").eyecatch)
# => "画像のパス"

フロント部分 (Vue.js)

Ⅰ. Webpacker をインストール

webpacker gem を追加する。

Gemfile
gem 'webpacker', '~> 3.5'

Webpacker をインストールします。

$ bundle
$ bundle exec rails webpacker:install

Ⅱ. Vue.js をインストール

Webpacker で Vue.js をインストールします。

$ bundle exec rails webpacker:install:vue

Ⅲ. Home ページを作成

Vue.js を返すための Home ページを作成します。

$ bundle exec rails g controller Pages Home

Root パスに Home ページを設定します。

config/routes.rb
Rails.application.routes.draw do

  root 'pages#home'

end

標準の JSON ではなく、ERB を返すために ActionController::Base に修正します。

app/controllers/pages_controller.rb
class PagesController < ActionController::Base
  def home
  end
end

Home ページに利用する View を作成します。

$ mkdir -p app/views/pages/
$ touch app/views/pages/home.html.erb

Ⅳ. Vue.js を利用

Vue.js を利用するための設定を行います。

app/views/pages/home.html.erb
<%= javascript_pack_tag 'main' %>

利用する各種ファイルを作成します。

$ touch app/javascript/packs/main.js
$ touch app/javascript/packs/App.vue
app/javascript/packs/main.js
import Vue from 'vue'
import App from './App.vue'

document.addEventListener('DOMContentLoaded', () => {
  const el = document.body.appendChild(document.createElement('main'))
  new Vue({
    el,
    render: h => h(App)
  })
})
app/javascript/packs/App.vue
<template>
  <div id="app">
    <p>投稿フォーム</p>
  </div>
</template>

Ⅴ. 投稿フォームを作成

API コールに利用する axios をインストールします。

$ yarn add axios

画像投稿するフォームを用意します。
画像は POST する前に Base64 にデコードしています。

app/javascript/packs/App.vue
<template>
  <div id="app">
    <p>投稿フォーム</p>
    <form v-on:submit.prevent="postItem()">
      <p>
        <label>Title</label>
        <input name="post.title" type="text" v-model="post.title"><br />
      </p>
      <p>
        <label>Body</label>
        <input name="post.body" type="text" v-model="post.body"><br />
      </p>
      <p>
        <label>画像</label>
        <input name="uploadedImage" type="file" ref="file" v-on:change="onFileChange()"><br />
      </p>
      <input type="submit" value="Submit">
    </form>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  data() {
    return {
      post: {},
      uploadedImage: ''
    }
  },
  methods: {
    onFileChange() {
      let file = event.target.files[0] || event.dataTransfer.files
      let reader = new FileReader()
      reader.onload = () => {
        this.uploadedImage = event.target.result
        this.post.image = this.uploadedImage
      }
      reader.readAsDataURL(file)
    },
    postItem() {
      return new Promise((resolve, _) => {
        axios({
          url: '/posts',
          data: {
            post: this.post
          },
          method: 'POST'
        }).then(res => {
          this.post = {}
          this.uploadedImage = ''
          this.$refs.file.value = ''
          resolve(res)
        }).catch(e => {
          console.log(e)
        })
      })
    }
  }
}
</script>

Ⅵ. 動作確認

アプリケーションを起動し、投稿フォームから画像を投稿します。
http://localhost:3000/

$ bundle exec rails s

img1.png

Rails Console でアイキャッチが正常に追加できているか確認します。

$ bundle exec rails c
irb(main):001:0> Post.last.eyecatch.attached?
=> true

参考記事

https://qiita.com/ozin/items/5ec81a4b126b8ebf7a96

最後に

読んでいただいてありがとうございます。
間違っている点などがありましたら、ご指摘いただけると喜びます!

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

CodinGame はBOT(AIプログラム)でバトルするのが正しい楽しみ方かもしれません

『【CodinGame】ブラウザでコーディングの基礎からトレーニングできるサイト (疑似ゲーム開発環境を使って学べます。解答は25種類のプログラミング言語から選択して記述可能!)』 という記事で、CodinGame に対してなにやら否定的なコメントを書いてしまいましたが、Twitter で「codingame」を検索してみると、「CodinGame はBOT(AIプログラム)でバトルするのが正しい楽しみ方」的な発言がみられたので、AIについては素人ながら挑戦してみました。

  • まだ、挑戦し始めなのでログ(ブログ)っぽく、やったことをそのまま記述…
  • 勝ち方の指南なんてできないので…「他の人が自分もやってみたい」と思えるような紹介風で…

という目標で書いてます。

長くなる(と思う)ので記事分けながら書いて、あとでマトメの記事が上手くできればいいなと考えています。

それでは、以下本文へ

1. https://www.codingame.com/start にアクセスします。

image.png

2. 「Sign up with Google」を選択します。

image.png

3. サインアップ完了後、https://www.codingame.com/home に自動的に遷移します。

image.png

4. https://www.codingame.com/multiplayer に遷移します。

image.png

5. https://www.codingame.com/multiplayer/bot-programming に遷移します。

image.png

6. TRON BATTLE のリプレイ動画

  • 以下のツイート内の画像をクリックすると「TRON BATTLE のリプレイ動画」に飛びます。
    この動画を見て分かるように、各プレイヤーの車は異なった色のリボンを残しながら進んでいきます。

  • 各プレイヤーの車は自分のリボンも他人のリボンも踏んではいけません(踏んだらアウト)。もちろん場外に出てもいけません。上記に違反した時点でそのプレイヤーはアウトとなり、そのプレイヤーのリボンがゲーム画面から消えます(ここもポイント!)。

  • 最後までアウトにならずに生き残ったプレイヤーの勝ちです。(実際には1位、2位、…と順位がつきます)

7. TRON BATTLE のメイン画面の「JOIN」を押してプログラム編集画面(IDE)を開きます

image.png

image.png

8. コードエディタに初期に表示される内容(C#の場合)

解答に使うプログラミング言語は、C#, C++, Java, Javascript, Python3, Bash, C, Clojure, Dart, F#, Go, Groovy, Haskell, Kotlin, Lua, ObjectiveC, OCaml, Pascal, Perl, PHP, Python2, Ruby, Rust, Scala, Swift, VB.NET の中から自由に選べます。

CodinGame のプログラミング問題はほとんど(全て?)、刻々と標準入力から情報を読み取り、刻々と標準出力に指示を書き出すというループから成り立っています。

  • このおかげでプログラミング言語間の差異を吸収しやすくなっています。ユーザーが書くプログラムを取り巻く親プロセスのプログラムは共通の物が使えるからです。
  • static void Main(string[] args) に合わせて、全プログラムを static メソッドで書こうとするとクラスを導入する際にハマることがありますのでご注意。
コードエディタに初期に表示される内容(C#の場合)
using System;
using System.Linq;
using System.IO;
using System.Text;
using System.Collections;
using System.Collections.Generic;

/**
 * Auto-generated code below aims at helping you parse
 * the standard input according to the problem statement.
 **/
class Player
{
    static void Main(string[] args)
    {
        string[] inputs;

        // game loop
        while (true)
        {
            inputs = Console.ReadLine().Split(' ');
            int N = int.Parse(inputs[0]); // total number of players (2 to 4).
            int P = int.Parse(inputs[1]); // your player number (0 to 3).
            for (int i = 0; i < N; i++)
            {
                inputs = Console.ReadLine().Split(' ');
                int X0 = int.Parse(inputs[0]); // starting X coordinate of lightcycle (or -1)
                int Y0 = int.Parse(inputs[1]); // starting Y coordinate of lightcycle (or -1)
                int X1 = int.Parse(inputs[2]); // starting X coordinate of lightcycle (can be the same as X0 if you play before this player)
                int Y1 = int.Parse(inputs[3]); // starting Y coordinate of lightcycle (can be the same as Y0 if you play before this player)
            }

            // Write an action using Console.WriteLine()
            // To debug: Console.Error.WriteLine("Debug messages...");

            Console.WriteLine("LEFT"); // A single line with UP, DOWN, LEFT or RIGHT
        }
    }
}

9. さて、TRON BATTLE の課題(問題)文は以下のような内容です

(長いので折りたたみ中。展開してご覧ください)

◎The Goal
◎目標

In this game your are a program driving the legendary tron light cycle and fighting against other programs on the game grid.

このゲームであなたが目指すのは、ゲームグリッド上で、伝説のトロンライトサイクルを運転して他のプログラムと戦うことのできるプログラムです。

The light cycle moves in straight lines and only turn in 90° angles while leaving a solid light ribbon in its wake. Each cycle and associated ribbon features a different color.
Should a light cycle stop, hit a light ribbon or goes off the game grid it will be instantly deactivated.

ライトサイクルは真っすぐに進むか90°の角度でしか曲がれず、起動時から固形分からなる光のリボンを残しながら進みます。それぞれのライトサイクルと関連付けられたリボンは異なる色を放ちます。
ライトサイクルが停止せざるを得ない、または光のリボンに衝突した、またはゲームグリッドの外に出た場合、そのライトサイクルは即座に非活性化されます。

The last cycle in play wins the game. Your goal is to be the best program: once sent to the arena, programs will compete against each-others in battles gathering 2 to 4 cycles. The more battles you win, the better your rank will be.

最後まで残ったライトサイクルがゲームの勝者となります。あなたの目標はベストプログラムを目指すことです: アリーナに送られれば(訳注: SUBMITボタンを押せば)、プログラム達が、2~4台でのライトサイクルバトルでお互いに競争となります。より多くかつほどあなたのランクが上がります。

◎Rules
◎ルール

Each battle is fought with 2 players. Each player plays in turn during a battle. When your turn comes, the following happens:

それぞれのバトルは2プレイヤーで戦います。それぞれのプレイヤーが順番にプレイします。あなたのターンが来たら、以下が発生します:

  • Information about the location of players on the grid is sent on the standard input of your program. So your AI must read information on the standard input at the beginning of a turn.
  • グリッド上のプレイヤーの位置情報があなたのプログラムの標準入力に送信されます。そのため、あなたのAIはターンの最初に標準入力上の情報を読み込まなければなりません。
  • Once the inputs have been read for the current game turn, your AI must provide its next move information on the standard ouput. The output for a game turn must be a single line stating the next direction of the light cycle: either UP, DOWN, LEFT or RIGHT.
  • 現在のゲームターンのための情報を読み込んだら、AIは次の移動のための情報を標準出力に提供しなければなりません。ゲームターン時の出力は、ライトサイクルの次の移動方向を宣言する一行の出力でなければなりません: UP, DOWN, LEFT, RIGHT のいずれかを出力します。
  • Your light cycle will move in the direction your AI provided.
  • あなたのライトサイクルはAIが出力した方向に動きます。
  • At this point your AI should wait for your next game turn information and so on and so forth. In the mean time, the AI of the other players will receive information the same way you did.
  • この時点で、あなたのAIは次のゲームターンの情報を待たなければなりません。後は、ここまでの繰り返しとなります。一方で、他のプレイヤーのAIもあなたと同様に情報を受け取ります。

If your AI does not provide output fast enough when your turn comes, or if you provide an invalid output or if your output would make the light cycle move into an obstacle, then your program loses.

もし、あなたのAIがあなたのターンが来た時に、十分高速に出力を提供できない場合、または妥当でない出力をした場合、または出力に従うとライトサイクルが障害物に衝突してしまう等の場合には、あなたのプログラムの負けとなります。

If another AI loses before yours, its light ribbon disappears and the game continues until there is only one player left.

もし他のAIがあなたより前に負けた場合は、その光のリボンは消滅し、一人のプレイヤーのみが残るまでゲームは継続します。

The game grid has a 30 by 20 cells width and height. Each player starts at a random location on the grid.

ゲームグリッドは、30x20 のセルで構成されます。それぞれのプレイヤーはグリッド上のランダムな位置からスタートします。

◎Victory Conditions
◎勝利条件

Be the last remaining player
最後まで残るプレイヤーとなること。

◎Game Input
◎ゲームの入力

Input for one game turn
ゲームターン毎の入力

Line 1: Two integers N and P. Where N is the total number of players and P is your player number for this game.

一行目: N と P の2つの整数。Nはプレイヤーの総人数で、Pはこのゲームでのプレイヤー番号です。

The N following lines: One line per player. First line is for player 0, next line for player 1, etc. Each line contains four values X0, Y0, X1 and Y1. (X0, Y0) are the coordinates of the initial position of the light ribbon (tail) and (X1, Y1) are the coordinates of the current position of the light ribbon (head) of the player. Once a player loses, his/her X0 Y0 X1 Y1 coordinates are all equal to -1 (no more light ribbon on the grid for this player).

続くN行: プレイヤー毎に一行。最初の行はプレイヤー0に対するもの、次の行はプレイヤー1、という形になります。それぞれの行は4つの値 X0, Y0, X1, Y1 を含みます。(X0, Y0) は光のリボンの初期位置(tail)で (X1, Y1) はプレイヤーの光のリボンの現在位置(head)です。あるプレイヤーの負けが決定すると、そのプレイヤーの X0 Y0 X1 Y1 の値hあ全て -1 となり、そのプレイヤーの光のリボンはグリッド上には存在しないことを意味します。

Output for one game turn
ゲームターン毎の出力

A single line with UP, DOWN, LEFT or RIGHT

UP, DOWN, LEFT, RIGHT のいずれかを一行で出力。

Constraints
制約

2 ≤ N ≤ 2
0 ≤ P < N
0 ≤ X0, X1 < 30
0 ≤ Y0, Y1 < 20

Your AI must answer in less than 100ms for each game turn.
あなたのAIは各ゲームターンに対して100ms未満で応答しなければなりません。

10. とりあえず、壁への激突、リボンへの激突を避ける目的で作ったプログラム

まったくAI的なことしてませんが、ゲームターン毎に隣(上下左右)のセルだけ見て、障害物がなければそちらに進む(判定順序: 左⇒右⇒上⇒下)。毎ターン、自キャラも含めて位置情報を二次元配列に格納(死んだキャラのリボン情報の消去も一応実装済み。初期は2キャラしかいないのでテストできませんw)。

  • コメントに大体書いたので一点だけ補足すると、ライトサイクルが曲がるとき90°までしか曲がれない(来た方向に戻るようなことはできない)というのをどう表現しようかと迷っていたんですが、自キャラの光のリボンも配列(マップ)に記録して障害物と見做しているので、とりあえず障害物判定するだけでいける方向に進めば良いことだと気づきました。
using System;
using System.Linq;
using System.IO;
using System.Text;
using System.Collections;
using System.Collections.Generic;

class Player
{
    static void Main(string[] args) {
        Player control = new Player();
        string[] inputs;
        Position[] positions;
        // game loop
        while (true) {
            inputs = Console.ReadLine().Split(' ');
            int N = int.Parse(inputs[0]); // total number of players (2 to 4).
            int P = int.Parse(inputs[1]); // your player number (0 to 3).
            Console.Error.WriteLine("P={0}", P);
            positions = new Position[N];
            for (int i = 0; i < N; i++) {
                inputs = Console.ReadLine().Split(' ');
                int X0 = int.Parse(inputs[0]); // starting X coordinate of lightcycle (or -1)
                int Y0 = int.Parse(inputs[1]); // starting Y coordinate of lightcycle (or -1)
                int X1 = int.Parse(inputs[2]); // starting X coordinate of lightcycle (can be the same as X0 if you play before this player)
                int Y1 = int.Parse(inputs[3]); // starting Y coordinate of lightcycle (can be the same as Y0 if you play before this player)
                positions[i] = new Position(i, X0, Y0, X1, Y1);
            }
            string dir = control.HandleVehicless(positions, P);
            control.DumpMap();
            Console.WriteLine(dir);
        }
    }
    // 自分も含めて誰かが通った座標を記憶しておくために使う。
    // 誰も通ってない場合は -1。通った、または現在いるマスに対してはプレイヤーのメンバーIDを格納する。
    int[,] map = new int[30,20];
    // メインコントロールクラスのコンストラクタ(map内の値を-1(=誰も通ってない)に初期化しておく。)
    private Player() {
        for(int y=0; y<20; y++) {
            for(int x=0; x<30; x++) {
                map[x, y] = -1;
            }
        }
    }
    private string HandleVehicless(Position[] positions, int myIndex) {
        Position me = positions[myIndex]; // me = 自分の座標情報
        Console.Error.WriteLine(me); // me を標準エラーに出力(public override string ToString()の定義による)
        foreach(var p in positions) { // (me も含めて)全キャラの座標を通ってはいけない場所に登録。
            AddToMap(p); // ただし、死にキャラの場合はそのキャラの座標情報を全消去する。
        }
        // 上下左右のマスを判定し通ってはいけない場所でなければその方向を返す。
        if (!FoundFromMap(me, -1, 0)) return "LEFT";
        if (!FoundFromMap(me, 1, 0)) return "RIGHT";
        if (!FoundFromMap(me, 0, -1)) return "UP";
        if (!FoundFromMap(me, 0, 1)) return "DOWN";
        return "LEFT"; // ここに来た時点でどの方向も通れないが一応正式な値の一つとして "LEFT" を返す。
    }
    void AddToMap(Position p) {
        if (p.X1 < 0) {
            DeleteMemberIdsFromMap(p.Id); // 現在座標がマイナス値で来たら死にキャラなのでマップから消す。
            return;
        }
        this.map[p.X1, p.Y1] = p.Id; // 配列にプレイヤーのメンバーIDを登録する。
    }
    void DeleteMemberIdsFromMap(int id) {
        for(int y=0; y<20; y++) {
            for(int x=0; x<30; x++) {
                if (map[x, y] == id) map[x, y] = -1;
            }
        }
    }
    // map を検索して通れない場所の場合 true を返す。通れる場合は false。
    // me(自機の座標)に xOffset と yOffset を加えた場所について判定(検索)する。
    bool FoundFromMap(Position me, int xOffset, int yOffset) {
        int x = me.X1+xOffset;
        int y = me.Y1+yOffset;
        if (x < 0) return true;
        if (x > 29) return true;
        if (y < 0) return true;
        if (y > 19) return true;
        return map[x, y] != -1;
    }
    // デバッグ用に 30x20 のマップを表示(現在生きているメンバーのIDを表示。空のマスは '-' を出力)
    void DumpMap() {
        for(int y=0; y<20; y++) {
            for(int x=0; x<30; x++) {
                if (map[x, y] == -1) Console.Error.Write("-"); // -1の場合はマイナス記号を出力。
                else Console.Error.Write(map[x, y]); // -1でなければプレイヤーID(0以上)を出力。
                Console.Error.Write(" ");
            }
            Console.Error.WriteLine();
        }
    }
}
// キャラクターの座標を登録・記憶しておくための入れ物。
// Main 関数が受け取る標準入力の情報を格納するための構造体のようなもの。
// Player インスタンスの各メソッドの引数は標準入力とのやり取りを意識せず、この構造体を期待できる。
class Position
{
    public int Id;
    public int X0;
    public int Y0;
    public int X1;
    public int Y1;
    public Position(int id, int x0, int y0, int x1, int y1)
    {
        this.Id = id;
        this.X0 = x0;
        this.Y0 = y0;
        this.X1 = x1;
        this.Y1 = y1;
    }
    // デバッグなどで出力される際のフォーマットを制御する。
    public override string ToString()
    {
        //return "{X0:" + X0 + ", Y0:" + Y0 + ", X1:" + X1 + ", Y1:" + Y1 + "}";
        return String.Format("{{X0:{0}, Y0:{1}, X1:{2}, Y1:{3}}}", X0, Y0, X1, Y1);
    }
}

11. アリーナでリーグ戦をする前に「PLAY MY CODE」ボタンで確認

image.png

12. 対戦実行速度を上げてサクサクデバッグ

image.png

image.png

13. アリーナ(リーグ戦)に挑戦

image.png

image.png

14. リーグ戦でボスに勝ったら以下のような画面が表示されます

  • 最初のリーグではプレイヤーの数は2ですが、リーグが上がっていくと増えていくみたいです。

image.png

15. リーグ戦で勝てず上位リーグに上がれなかった場合の対処法

image.png

image.png

image.png

image.png

他にも負けた相手がいる場合には、同様の手順でIDEに読み込んで対戦しながらプログラム(AI)を強くするとよいでしょう。

16. 最後に

AIを作るノウハウを持っていないことと、強いプログラムを記事で晒すのはいいアイディアではないかなと思ってますので、今回は TRON BATTLE を紹介しましたが、次はまた別のプログラムについて紹介したいと思っています。以下のツイートの画像をクリックしていただければ、そのゲームのリプレイ画面が表示されます。
もし、分かりにくいところなどありましたらコメント等をよろしくお願いいたします。それでは…

P.S.

https://www.codingame.com/multiplayer/bot-programming/tron-battle/leaderboard

image.png

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