- 投稿日:2019-11-24T21:40:12+09:00
TataraというRubyで型を使えるライブラリを作ってみた
はじめに
この記事は最近僕が作っているRubyで型を使えるライブラリの紹介記事です。
対象読者としては、Rubyで簡単に型のようなものを使ってみたい人、またはC++でのRuby拡張を作ってみたい人を想定しています。
Tataraの紹介をしつつ、簡単なC++でのRuby拡張実装についても触れていきます。
作ったもの
TataraというRubyで簡単な型を使えるようにするRubyExtensionを作りました。RubyGemsにもリリースしており、v0.2.0が現在リリースされている最新バージョンになります。
v0.2.0まではRiceというRubyExtensionを簡単にC++で使用していました。しかし、GCへの管理追加が少しややこしかったり、RiceでC++のコードをRuby向けにラップしているため処理が遅くなったりするという問題がありました。そこで次期リリースではC++のみでの実装に書き直し、GCへの管理追加や処理速度向上を図っています。
ちなみに、次にリリースするv0.3.0は11月中にリリースする予定で実装を進めています。
サンプルコード
例えば、Tataraを使うことで以下のようなコードを実行できます。
require 'tatara' @int_container = Tatara::IntVector.new (1..10).each{|i| @int_container << i} # Insert 1..10 to @int_container @int_container.map{|i| puts i # 1..10 value is shown }
@int_container
は整数値のみが保存できるVector
になります。浮動小数点数値なども代入できますが、整数値に暗黙的に変換されます。@int_container = Tatara::IntVector.new @int_container << 4.2 # => Push 4ちなみに、文字列を保存しようとするとエラーになります。
@int_container = Tatara::IntVector.new @int_container << "42" # => Error!このようにTataraでは簡単な型が使えるようにしてあります。
作ろうと思ったきっかけ
Ruby自体は非常に書きやすく、楽しくコーディングができる素晴らしい言語です。
ですが、元々静的型付け言語(C/C++など)を触ることが多かったため「Rubyにも型のようなものが欲しいなぁ……」と思うこともありました。
そんな折、RiceやextppなどでRubyExtensionを作れそうだとわかり、「面白そうなので作ってみよう
!」となり作ってみました。やったこと
開発初期のころはRiceでのRubyExtension作成についてドキュメントや記事などを漁りに漁っていました。で、Riceで実装できそうなことがわかったので少しづつ実装を進めていった感じですね。
最初はIntegerやFloatなどを実装し、その後VectorやMapなどを実装していきました。
v0.1.0をリリースした辺りでGCへの管理追加などがややこしいことが判明し、Riceを外す方法を模索し始めました。extppなどでのRubyExtension実装ついても調べたりしました。
結果として、Ruby自体の実装コードなどを読みつつ、以下の記事を参考にRiceを外していくことにしました。そのおかげでRubyの実装なども読むことができたので良い経験が積めたと思います。
Riceでの実装
Riceでは以下のようにC++で作成したクラスをラップしてRuby向けに使用できるようにできます。
#include <rice/Data_Type.hpp> #include <rice/Constructor.hpp> using namespace Rice; class Integer { int value = 0; public: Integer(); ~Integer(); int assignment(const int var); int return_value(); }; Integer::Integer() {} Integer::~Integer(){} int Integer::assignment(const int var) { return this->value = var; } int Integer::return_value() { return this->value; } extern "C" { void Init_tatara() { Module rb_mTatara = define_module("Tatara"); Data_Type<Integer> rbcInteger = define_class_under<Integer>(rb_mTatara, "Integer") .define_constructor(Constructor<Integer>()) .define_method("value", &Integer::return_value) .define_method("value=", &Integer::assignment); } }RiceではC++で定義したクラスを
Data_Type
でRubyのクラスへとラップしてくれます。またモジュール名などはModule rb_mTatara = define_module("Tatara");
のように作成することができます。Module rb_mTatara = define_module("Tatara"); Data_Type<Integer> rbcInteger = define_class_under<Integer>(rb_mTatara, "Integer")上記のコードでは
Tatara
モジュール内にInteger
クラスを作成しています。.define_constructor(Constructor<Integer>())またこのコードではC++の
Integer
クラスのコンストラクタをnew
として利用しています。あとは、以下のように必要なメソッドを
define_method
でクラスに追加しています。.define_method("value", &Integer::return_value) .define_method("value=", &Integer::assignment);
define_method
はメソッド名と実行するメソッドを引数として渡すことで、C++側で定義したメソッドをRuby側で使用することができます。これにより、以下のようなRubyのコードが実行できます。
@integer = Tatara::Integer.new @integer.value = 42 puts @integer.value # => 42このようにRiceを使うことでC++で定義したクラスをRubyで使用することができます。
ちなみに、テンプレートなどを使う場合は以下のように型を指定する必要があります。
Data_Type<CppArray<int>> rb_cIntArray = define_class_under<CppArray<int>>(rb_mTatara, "IntArray") .define_constructor(Constructor<CppArray<int>>());C++への実装書き直し
先ほどRiceで書いていたコードは以下のように書き直すことができます。
#include <ruby.h> class Integer { int value = 0; public: Integer(); ~Integer(); int assignment(const int var); int return_value(); }; Integer::Integer() {} Integer::~Integer(){} int Integer::assignment(const int var) { return this->value = var; } int Integer::return_value() { return this->value; } static Integer *getInteger(VALUE self) { Integer *ptr; Data_Get_Struct(self, Integer, ptr); return ptr; } static void wrap_int_free(Integer *ptr) { ptr->~Integer(); ruby_xfree(ptr); } static VALUE wrap_int_alloc(VALUE klass) { void *p = ruby_xmalloc(sizeof(Integer)); p = new Integer; return Data_Wrap_Struct(klass, NULL, wrap_int_free, p); } static VALUE wrap_int_init(VALUE self) { Integer *p = getInteger(self); p = new Integer; return Qnil; } static VALUE wrap_int_return_value(VALUE self) { const int value = getInteger(self)->return_value(); VALUE result = INT2NUM(value); return result; } static VALUE wrap_int_assignment(VALUE self, VALUE value) { const int v = NUM2INT(value); const int r = getInteger(self)->assignment(v); VALUE result = INT2NUM(r); return result; } extern "C" { void Init_tatara() { VALUE mTatara = rb_define_module("Tatara"); VALUE rb_cInteger = rb_define_class_under(mTatara, "Integer", rb_cObject); rb_define_alloc_func(rb_cInteger, wrap_int_alloc); rb_define_private_method(rb_cInteger, "initialize", RUBY_METHOD_FUNC(wrap_int_init), 0); rb_define_method(rb_cInteger, "value", RUBY_METHOD_FUNC(wrap_int_return_value), 0); rb_define_method(rb_cInteger, "value=", RUBY_METHOD_FUNC(wrap_int_assignment), 1); } }C++のクラスをそのままRuby側で呼び出すことはできないので、以下のような関数を作成し、Ruby側から呼び出せるようにしています。
static Integer *getInteger(VALUE self) { Integer *ptr; Data_Get_Struct(self, Integer, ptr); return ptr; } static void wrap_int_free(Integer *ptr) { ptr->~Integer(); ruby_xfree(ptr); } static VALUE wrap_int_alloc(VALUE klass) { void *p = ruby_xmalloc(sizeof(Integer)); p = new Integer; return Data_Wrap_Struct(klass, NULL, wrap_int_free, p); } static VALUE wrap_int_init(VALUE self) { Integer *p = getInteger(self); p = new Integer; return Qnil; }モジュールは
rb_define_module
で作成でき、以下のようにTatara
というモジュールを作成しています。VALUE mTatara = rb_define_module("Tatara");また、以下のコードでは
Integer
クラスをTatara
モジュール内に作成し、その基底クラスにObject
クラスを使用しています。VALUE rb_cInteger = rb_define_class_under(mTatara, "Integer", rb_cObject);Rubyのソースコード内では
VALUE
型を使うことでインスタンスの値などを取得することができています。
getIntegr
ではインスタンス自身からC++のクラスのポインタを取得し、そのポインタを使ってクラスで定義されているメソッドを実行しています。
例えば以下のようにreturn_value
メソッドを呼び出すことができます。static VALUE wrap_int_return_value(VALUE self) { const int value = getInteger(self)->return_value(); VALUE result = INT2NUM(value); return result; }また
wrap_int_free
ではRubyのGCでメモリが解放される際の処理を実装しています。static void wrap_int_free(Integer *ptr) { ptr->~Integer(); ruby_xfree(ptr); }
wrap_int_alloc
はRubyのコードで新しくnew
でインスタンスを作成した際のアロケーションを実装しています。static VALUE wrap_int_alloc(VALUE klass) { void *p = ruby_xmalloc(sizeof(Integer)); p = new Integer; return Data_Wrap_Struct(klass, NULL, wrap_int_free, p); }
rb_define_alloc_func
でアロケーション時に実行する関数を指定できます。rb_define_alloc_func(rb_cInteger, wrap_int_alloc);
wrap_int_init
はinitialize
を実装しています。static VALUE wrap_int_init(VALUE self) { Integer *p = getInteger(self); p = new Integer; return Qnil; }最後に、以下のようにC++で定義されたクラスのメソッドを実行するラップ関数を作成します。
static VALUE wrap_int_return_value(VALUE self) { const int value = getInteger(self)->return_value(); VALUE result = INT2NUM(value); return result; } static VALUE wrap_int_assignment(VALUE self, VALUE value) { const int v = NUM2INT(value); const int r = getInteger(self)->assignment(v); VALUE result = INT2NUM(r); return result; }
INT2NUM()
やNUM2INT()
はCとRubyとの間でデータの変換を行う処理になります。この処理を間に挟むことでRuby内の変数の値などをC++側に渡すことができます。あとは、作成したラップ関数を以下のようにメソッドとして追加します。
rb_define_method(rb_cInteger, "value", RUBY_METHOD_FUNC(wrap_int_return_value), 0); rb_define_method(rb_cInteger, "value=", RUBY_METHOD_FUNC(wrap_int_assignment), 1);
rb_define_method
は第一引数にメソッドを追加するクラスのVALUE
を渡します。第二引数にはメソッド名、第三引数には実行する関数を渡します。
RUBY_METHOD_FUNC
では渡している関数ポインタをRubyのメソッドとしてキャストしています。
第四引数は、そのメソッドが受け取る引数の数になります。0
の場合は引数はなく、1
などの場合はその数だけ引数を受け取ります。なお、-1
の場合は可変長引数として受け取っているようです(RubyのArrayクラスの実装などで見る限り)。このようにしてC++でRubyのクラスを作成することができます!
おわりに
今回作成した
Tatara
の紹介をしつつ、C++でのRuby拡張の実装などについても触れてみました。普段使っているRubyの内部コードについても少し触れましたので、興味が湧いた方もおられるかもしれません。ここから宣伝。
なお、東京近隣の方であればRuby Hacking Challenge HolidayというRubyの実装などをRubyコミッターから直々に聴けるというイベントがあります!
ちなみに、次回は「Ruby 2.7新機能紹介」や「Rails Girls Tokyo, More! 参加者の皆様と合同LT大会」を行うようです。Ruby Hack Challenge Holiday #9 Ruby 2.7 + 年末LT大会
この記事を読んでRubyの内部に興味を持った方は是非参加してみるといいでしょう。
ちなみに、島根県浜田市ではRuby Hacking Challenge in Hamada.rbをHamada.rb内で開催しています。
僕のほうで内部のコードなど解説を行いますので、興味のある方は是非ご参加ください(なお、わかる範囲になりそうですが……)Ruby Hacking Challenge in Hamada.rb
宣伝終わり
今後もゆっくりと
Tatara
の実装を行いつつ、Ruby自体の実装への理解を深めていきたいと思います。参考資料
- 投稿日:2019-11-24T20:59:31+09:00
正規表現 アンカー
アンカーは幅0の文字列にマッチするメタ文字列です。 幅0とは文字と文字の間、文字列の先頭、文字列の末尾、 などを意味します。 ある特定の条件を満たす「位置」にマッチします。
^ 行頭にマッチします。行頭とは、文字列の先頭もしくは改行の次を 意味します。
$ 行末にマッチします。 行末とは文字列の末尾もしくは改行の手前を意味します。
\A 文字列の先頭にマッチします。
\Z 文字列の末尾にマッチします。 ただし文字列の最後の文字が改行ならばそれの手前にマッチします。
\z 文字列の末尾にマッチします。
\b 単語境界にマッチします。 単語を成す文字と単語を成さない文字の間にマッチします。 文字列の先頭の文字が単語成す文字であれば、文字列の先頭 の位置にマッチします。
\B 非単語境界にマッチします。 \bでマッチしない位置にマッチします。
# 文字列中の real にマッチする
/real/.match("surrealist") # => #< MatchData "real">
# 先頭に real とないとマッチしない
/\Areal/.match("surrealist") # => nil
# 単語境界がrealの前にないのでマッチしない
/\breal/.match("surrealist") # => nil
単語を成す文字、成さない文字の定義はエンコードによって 異なります。以下の例で「全角」括弧は EUC-JP では 単語を成す文字と見なされますが、UTF-8 では見なされません。 その結果、以下のような挙動をします。# -- coding:utf-8 --
# デフォルトは UTF-8
/foo\b/.match("あいうfoo%") # => #< MatchData "foo">
/\bfoo\b/.match("あいうfoo%") # => nil
/\bfoo\b/e.match("(foo)".encode("EUC-JP")) # => nil
/\bfoo\b/.match("(foo)") # => #< MatchData "foo">
Unicode の規格では、単語を成す文字を Word というプロパティで 定義しています。先読み、後読み(lookahead, lookbehind)
ある位置から続く文字列が ある部分式にマッチするならばその位置にマッチする という正規表現を書くことができます。「ある位置から続く文字列(先読み、lookahead)/ある位置の手前までの文字列(後読み、lookbehind)」 と「マッチする(肯定、positive)/マッチしない(否定、negative)」 の組み合わせで4つのパターンがあります。
(?=pat) 肯定先読み(positive lookahead)
(?!pat) 否定先読み(negative lookahead)
(?<=pat) 肯定後読み(positive lookbehind)
(?<!pat) 否定後読み(negative lookbehind)
\K 後読みの別表記、このメタ文字列の手前までを後読みします。 つまり /pat1\Kpat2/ は /(?<=pat1)pat2/ と同様の意味となります。
# 以下の例では、後読みと先読みを使って < b> と
# に挟まれているという条件を正規表現中に記述しつつ
# 自体にはマッチさせていない。
/(?<=< b>)\w+(?=<\/b>)/.match("Fortune favours the < b>bold< /b>")
# => #< MatchData "bold">
# 以下は上の正規表現と同じものを表す
/< b>\K\w+(?=<\/b>)/.match("Fortune favours the < b>bold< /b>")
# => #< MatchData "bold">
- 投稿日:2019-11-24T20:53:46+09:00
正規表現 キャプチャ
丸括弧 ( ) によってキャプチャをすることができます。 括弧に囲まれた部分正規表現にマッチした 前からn番目の開き括弧によって囲まれた部分式にマッチした 文字列を後で参照することができます。
正規表現内では \1, \2, ... という記法で後方参照できます。 また、\k<1>, \k<2>, ... や \k'1', \k'2', ... という記法を使うこと もできます(10を越える数字を渡すことができます)。 また、Regexp#match で得られた MatchData からは MatchData#[ ]で取り出せます。
また、$1, $2, ... という特殊変数によって n 番目の括弧にマッチした 部分文字列を参照できます。これらの特殊変数はマッチ処理が終わったあとで しか使えないことに注意してください。
# (..) に at がマッチしたのを \1 で参照し、マッチが成功している。
m = /csh [csh]\1 in/.match("The cat sat in the hat")
# => #< MatchData "cat sat in" 1:"at">
# Regexp#match でマッチしたテキストは MatchData#[] で参照できる
m[1] # => "at"
1,2,... ではなく、名前を付けることができます。 (?< name>pat)もしくは(?'name'pat)と記述します。キャプチャした文字列は MatchData#[ ] に Symbol を渡すことで参照できます。 これは名前付きキャプチャと呼ばれます。m = /\$(?\d+).(?< cents>\d+)/.match("$3.67")
# => #< MatchData "$3.67" dollars:"3" cents:"67">
m[:dollars] # => "3"
m[:cents] # => "67"
名前付きキャプチャは正規表現内で \k、\k'name' という記法で参照できます。/(?< vowel>[aeiou]).\k< vowel>.\k< vowel>/.match('ototomy')
# => #< MatchData "ototo" vowel:"o">
注: 名前付きキャプチャと数字によるキャプチャは併用できません。リテラル正規表現内に名前付きキャプチャがあり、 =~ の左辺で用いた 場合には、その名前のローカル変数にキャプチャした文字列を代入します。
/\$(?< dollars>\d+).(?< cents>\d+)/ =~ "$3.67" # => 0
dollars # => "3"
cents # => "67"
注: ローカル変数への代入が行われるのは、左辺の正規表現リテラルが#{}による式展開を含んでいない場合に限られます。数字による後方参照では、負の数による、 \k<-1>, \k<-2>, ... や \k'-1', \k'-2', ... という記法での 相対的な指定が可能です。-1 は後方参照が書かれた位置の1つ手前の 位置にあるキャプチャを表し、-2, -3, で2つ手前、3つ手前を表します。 これは非常に多くのキャプチャを持つような正規表現を記述するためや、 正規表現に別の正規表現を式展開で埋め込む場合などに便利です。
/(.)(.)\k<-2>\k<-1>/.match("xyzyz") # => #< MatchData "yzyz" 1:"y" 2:"z">
- 投稿日:2019-11-24T20:40:48+09:00
正規表現 繰り返し
以下のメタ文字列は繰り返しを表現します。 直前の部分式を何回繰り返すかを指定します。このような繰り返しを 表すメタ文字列を量指定子(quantifier)と呼びます。
* 0回以上
+ 1回以上
? 0回もしくは1回
{n} ちょうどn回(nは数字)
{n,} n回以上(nは数字)
{,m} m回以下(mは数字)
{n,m} n回以上m回以下(n,mは数字)
以下の例で、量指定子の基本的な使いかたを示しています。# 以下の正規表現は 最初に大文字が1文字以上(H)で、小文字が1文字以上(l)、
# lが2文字(ll)の後ろにoが続く文字列にマッチします。
"Hello".match(/[[:upper:]]+[[:lower:]]+l{2}o/) # => #< MatchData "Hello">
これらは「欲張り(greedy)」にマッチします。 マッチが成功する、最長の文字列にマッチしようとします。 そのため、これらの量指定子は特に最大量指定子(greedy quantifier) と呼ばれます。最小量指定子(reluctant quantifier)
一方、以下のメタ文字列(普通の繰り返しメタ文字列に ? を付加したもの) はマッチが成功する、最短の文字列にマッチします。そのため、これらの 量指定子は特に最小量指定子(reluctant quantifier)と呼ばれます。*? 0回以上
+? 1回以上
?? 0回もしくは1回
{n,}? n回以上(nは数字)
{,m}? m回以下(mは数字)
{n,m}? n回以上m回以下(n,mは数字)
以下の例では、最小量指定子を使うことで、(\d+)がマッチする場所を変えています。/^.(\d+)./.match("Copyright 2013.") # => #< MatchData "Copyright 2013." 1:"3">
/^.?(\d+)./.match("Copyright 2013.") # => #< MatchData "Copyright 2013." 1:"2013">
また、ネストしていない括弧の対応を取るためにも使えます。# ここでは < b> と < /b> の対応を取る
%r{< b>.< /b>}.match("< b>x< /b>y< b>z< /b>") # => #x< /b>y< b>z< /b>">
%r{< b>.?< /b>}.match("< b>x< /b>y< b>z< /b>") # => #< MatchData "< b>x< /b>">
絶対最大量指定子(possessive quantifier)
以下のメタ文字列は、最大量指定子のように最長のマッチをしますが、 一度マッチすると、その後マッチに失敗してもバックトラックしません。 つまりマッチ済みの文字列を手放さずにマッチに失敗します。 これらの量指定子は絶対最大量指定子と呼ばれます。*+ 0回以上
++ 1回以上
?+ 0回もしくは1回
アトミックグループを用いることで同じことができます。
- 投稿日:2019-11-24T20:20:23+09:00
文字クラス
文字クラス(character class) とは角括弧 [ と ] で囲まれ、1個以上の文字を列挙したもので、 いずれかの1文字にマッチします。
/W[aeiou]rd/
は Ward, Werd, Wird, Word, Wurd のいずれかにマッチします。文字クラス内のハイフン(-)は文字の範囲を表すメタ文字です。 例えば [abcd] という文字クラスは [a-d] と表すことができます。 複数の範囲指定をすることもできます。例えば [abcdpqrs] は [a-dp-s]と 表すこともできます。
文字クラスの [ の直後の文字がキャレット(^)である場合、列挙「されていない」 文字にマッチするようになります(これは否定文字クラスと呼ばれます)。
[^a-d]
はabcd以外の1文字にマッチします。文字クラス内に別の文字クラスを含めることができます。 [a-z[0-9]] は [a-z0-9]と同じ意味を持ちます。これだけではあまり意味が ありませんが、文字クラスは && という、共通部分を取る演算をサポートして いるため、これと組合せることで意味を持ちます。
/[a-z[0-9]]/.match("y") # => #< MatchData "y">
/[a-z[0-9]]/.match("[") # => nil
r = /[a-w&&[^c-g]e]/ # ([a-w] かつ ([^c-g] もしくは e)) つまり [abeh-w] と同じ
r.match("b") # => #< MatchData "b">
r.match("c") # => nil
r.match("e") # => #
r.match("g") # => nil
r.match("h") # => #< MatchData "h">
r.match("w") # => #< MatchData "w">
r.match("z") # => nil
文字クラスでは、否定(^)範囲(-)共通部分(&&)列挙(並べる)という 演算が可能ですが、これらは - > (列挙) > && > ^ という順の結合強度を持ちます。文字クラス内の3つのメタ文字を通常の文字の意味で使用したい場合には、 \ によってエスケープ する必要があります。
文字クラスの略記法
良く使われる文字クラスには省略記法が存在します。\w 単語構成文字 [a-zA-Z0-9_]
\W 非単語構成文字 [^a-zA-Z0-9_]
\s 空白文字 [ \t\r\n\f\v]
\S 非空白文字 [^ \t\r\n\f\v]
\d 10進数字 [0-9]
\D 非10進数字 [^0-9]
\h 16進数字 [0-9a-fA-F]
\H 非16進数字 [^0-9a-fA-F]
これらの「空白」「数字」などは ASCII の範囲の文字のみを対象としています。 いわゆる「全角アルファベット」「全角空白」「全角数字」などは ここの空白、数字、には含まれません。/\w+/.match("ABCdef") # => nil
/\W+/.match("ABCdef") # => #< MatchData "ABCdef">
/\s+/.match(" ") # => nil
/\S+/.match(" ") # => #< MatchData " ">
これらは文字クラス内で演算することもできます。r = /[\d&&[^47]]/ # 4, 7 以外の数字
r.match("3") # => #< MatchData "3">
r.match("7") # => nil
Unicode プロパティによる文字クラス指定
また、Unicodeのプロパティ(属性情報)による文字クラス指定も可能です。 以下の記法が使えます。\p{property-name}
\p{^property-name} (否定)
\P{property-name} (否定)
サポートされているプロパティのリストは https://github.com/k-takata/Onigmo/blob/master/doc/UnicodeProps.txt を 参考にしてください。また、プロパティの意味は Unicode の仕様を参照してください。/\p{Letter}+/.match(".|あaABc123") # => #< MatchData "あaABc">
POSIX 文字クラス
Unicodeプロパティと 似た機能を持つ記法として、POSIX 文字クラスと呼ばれるものがあります。 これらは上の省略記法とは異なり、文字クラスの中でしか用いることが できません。これらは [:クラス名:] という記法を持ちます。 また、[:^クラス名:]という記法でその否定を意味します。 以下の括弧では実際にどの文字にマッチするかが Unicode プロパティや Unicode コードポイントで示されています。[:alnum:] 英数字 (Letter | Mark | Decimal_Number)
[:alpha:] 英字 (Letter | Mark)
[:ascii:] ASCIIに含まれる文字 (0000 - 007F)
[:blank:] スペースとタブ (Space_Separator | 0009)
[:cntrl:] 制御文字 (Control | Format | Unassigned | Private_Use | Surrogate)
[:digit:] 数字 (Decimal_Number)
[:graph:] 空白以外の表示可能な文字(つまり空白文字、制御文字、以外) ([[:^space:]] && ^Control && ^Unassigned && ^Surrogate)
[:lower:] 小文字 (Lowercase_Letter)
[:print:] 表示可能な文字(空白を含む) ([[:graph:]] | Space_Separator)
[:punct:] 句読点 (Connector_Punctuation | Dash_Punctuation | Close_Punctuation | Final_Punctuation | Initial_Punctuation | Other_Punctuation | Open_Punctuation)
[:space:] 空白、改行、復帰 (Space_Separator | Line_Separator | Paragraph_Separator | 0009 | 000A | 000B | 000C | 000D | 0085)
[:upper:] 大文字 (Uppercase_Letter)
[:xdigit:] 16進表記で使える文字 (0030 - 0039 | 0041 - 0046 | 0061 - 0066)
[:word:] 単語構成文字 (Letter | Mark | Decimal_Number | Connector_Punctuation)
これらの POSIX 文字クラスは \s といった省略記法と異なり、 ASCIIコード範囲外の空白などを考慮に入れます。/[[:alnum:]]+/.match("abAB121") # => #< MatchData "abAB121">
#\u3000 は全角空白
/[[:graph:]]/.match("\u3000") # => nil
/[[:blank:]]/.match("\u3000") # => #
/[[:alnum:]&&[:^lower:]]/.match("aA") # => #< MatchData "A">
/[[:print:]&&[:^lower:]]/.match(" ") # => #< MatchData " ">
注: POSIX ではここで言う文字クラスのことを「ブラケット表現」と 呼び、[:xxx:] というのを文字クラス、と呼んでいます。よって、 POSIX文字クラス、というのは厳密には 「POSIXブラケット表現における文字クラス」と呼ぶべきものですが、 ここでは「POSIX文字クラス」と呼ぶことにします。注: [:word:] と [:ascii:] は POSIX では定義されていません。 Ruby/Oniguruma/Onigmo独自のものです。
注: エンコーディングによってこれらの POSIX 文字クラスの挙動が 異なります。上に書いている「マッチする文字」は Unicode 系統の エンコーディングで使われるものです。 Unicode 系統以外のものは Onigmo の ドキュメントを参照してください。
オプション
文字クラスの挙動は オプション で変更することができます。 d, a, u の3つのオプションがあります。
- 投稿日:2019-11-24T20:20:23+09:00
正規表現 文字クラス
文字クラス(character class) とは角括弧 [ と ] で囲まれ、1個以上の文字を列挙したもので、 いずれかの1文字にマッチします。
/W[aeiou]rd/
は Ward, Werd, Wird, Word, Wurd のいずれかにマッチします。文字クラス内のハイフン(-)は文字の範囲を表すメタ文字です。 例えば [abcd] という文字クラスは [a-d] と表すことができます。 複数の範囲指定をすることもできます。例えば [abcdpqrs] は [a-dp-s]と 表すこともできます。
文字クラスの [ の直後の文字がキャレット(^)である場合、列挙「されていない」 文字にマッチするようになります(これは否定文字クラスと呼ばれます)。
[^a-d]
はabcd以外の1文字にマッチします。文字クラス内に別の文字クラスを含めることができます。 [a-z[0-9]] は [a-z0-9]と同じ意味を持ちます。これだけではあまり意味が ありませんが、文字クラスは && という、共通部分を取る演算をサポートして いるため、これと組合せることで意味を持ちます。
/[a-z[0-9]]/.match("y") # => #< MatchData "y">
/[a-z[0-9]]/.match("[") # => nil
r = /[a-w&&[^c-g]e]/ # ([a-w] かつ ([^c-g] もしくは e)) つまり [abeh-w] と同じ
r.match("b") # => #< MatchData "b">
r.match("c") # => nil
r.match("e") # => #
r.match("g") # => nil
r.match("h") # => #< MatchData "h">
r.match("w") # => #< MatchData "w">
r.match("z") # => nil
文字クラスでは、否定(^)範囲(-)共通部分(&&)列挙(並べる)という 演算が可能ですが、これらは - > (列挙) > && > ^ という順の結合強度を持ちます。文字クラス内の3つのメタ文字を通常の文字の意味で使用したい場合には、 \ によってエスケープ する必要があります。
文字クラスの略記法
良く使われる文字クラスには省略記法が存在します。\w 単語構成文字 [a-zA-Z0-9_]
\W 非単語構成文字 [^a-zA-Z0-9_]
\s 空白文字 [ \t\r\n\f\v]
\S 非空白文字 [^ \t\r\n\f\v]
\d 10進数字 [0-9]
\D 非10進数字 [^0-9]
\h 16進数字 [0-9a-fA-F]
\H 非16進数字 [^0-9a-fA-F]
これらの「空白」「数字」などは ASCII の範囲の文字のみを対象としています。 いわゆる「全角アルファベット」「全角空白」「全角数字」などは ここの空白、数字、には含まれません。/\w+/.match("ABCdef") # => nil
/\W+/.match("ABCdef") # => #< MatchData "ABCdef">
/\s+/.match(" ") # => nil
/\S+/.match(" ") # => #< MatchData " ">
これらは文字クラス内で演算することもできます。r = /[\d&&[^47]]/ # 4, 7 以外の数字
r.match("3") # => #< MatchData "3">
r.match("7") # => nil
Unicode プロパティによる文字クラス指定
また、Unicodeのプロパティ(属性情報)による文字クラス指定も可能です。 以下の記法が使えます。\p{property-name}
\p{^property-name} (否定)
\P{property-name} (否定)
サポートされているプロパティのリストは https://github.com/k-takata/Onigmo/blob/master/doc/UnicodeProps.txt を 参考にしてください。また、プロパティの意味は Unicode の仕様を参照してください。/\p{Letter}+/.match(".|あaABc123") # => #< MatchData "あaABc">
POSIX 文字クラス
Unicodeプロパティと 似た機能を持つ記法として、POSIX 文字クラスと呼ばれるものがあります。 これらは上の省略記法とは異なり、文字クラスの中でしか用いることが できません。これらは [:クラス名:] という記法を持ちます。 また、[:^クラス名:]という記法でその否定を意味します。 以下の括弧では実際にどの文字にマッチするかが Unicode プロパティや Unicode コードポイントで示されています。[:alnum:] 英数字 (Letter | Mark | Decimal_Number)
[:alpha:] 英字 (Letter | Mark)
[:ascii:] ASCIIに含まれる文字 (0000 - 007F)
[:blank:] スペースとタブ (Space_Separator | 0009)
[:cntrl:] 制御文字 (Control | Format | Unassigned | Private_Use | Surrogate)
[:digit:] 数字 (Decimal_Number)
[:graph:] 空白以外の表示可能な文字(つまり空白文字、制御文字、以外) ([[:^space:]] && ^Control && ^Unassigned && ^Surrogate)
[:lower:] 小文字 (Lowercase_Letter)
[:print:] 表示可能な文字(空白を含む) ([[:graph:]] | Space_Separator)
[:punct:] 句読点 (Connector_Punctuation | Dash_Punctuation | Close_Punctuation | Final_Punctuation | Initial_Punctuation | Other_Punctuation | Open_Punctuation)
[:space:] 空白、改行、復帰 (Space_Separator | Line_Separator | Paragraph_Separator | 0009 | 000A | 000B | 000C | 000D | 0085)
[:upper:] 大文字 (Uppercase_Letter)
[:xdigit:] 16進表記で使える文字 (0030 - 0039 | 0041 - 0046 | 0061 - 0066)
[:word:] 単語構成文字 (Letter | Mark | Decimal_Number | Connector_Punctuation)
これらの POSIX 文字クラスは \s といった省略記法と異なり、 ASCIIコード範囲外の空白などを考慮に入れます。/[[:alnum:]]+/.match("abAB121") # => #< MatchData "abAB121">
#\u3000 は全角空白
/[[:graph:]]/.match("\u3000") # => nil
/[[:blank:]]/.match("\u3000") # => #
/[[:alnum:]&&[:^lower:]]/.match("aA") # => #< MatchData "A">
/[[:print:]&&[:^lower:]]/.match(" ") # => #< MatchData " ">
注: POSIX ではここで言う文字クラスのことを「ブラケット表現」と 呼び、[:xxx:] というのを文字クラス、と呼んでいます。よって、 POSIX文字クラス、というのは厳密には 「POSIXブラケット表現における文字クラス」と呼ぶべきものですが、 ここでは「POSIX文字クラス」と呼ぶことにします。注: [:word:] と [:ascii:] は POSIX では定義されていません。 Ruby/Oniguruma/Onigmo独自のものです。
注: エンコーディングによってこれらの POSIX 文字クラスの挙動が 異なります。上に書いている「マッチする文字」は Unicode 系統の エンコーディングで使われるものです。 Unicode 系統以外のものは Onigmo の ドキュメントを参照してください。
オプション
文字クラスの挙動は オプション で変更することができます。 d, a, u の3つのオプションがあります。
- 投稿日:2019-11-24T20:18:04+09:00
正規表現 文字
正規表現内では、「\」の後に文字列を置くことで、 ある特定の文字を表現することができます。 これは、改行のように Ruby の文法で特別な意味を持つ文字を埋め込む ためなどに用いられます。 文字列リテラルの記法とほぼ同様(リテラル/バックスラッシュ記法)で、 以下の記法が利用可能です。
\t 水平タブ horizontal tab (0x09)
\v 垂直タブ vertical tab (0x0B)
\n 改行 newline (0x0A)
\r 復帰 return (0x0D)
\b バックスペース back space (0x08)
\f 改ページ form feed (0x0C)
\a ベル bell (0x07)
\e エスケープ文字 escape (0x1B)
\nnn 符号化バイト値の8進数表現 (nnn の8進数3文字で表現)
\xHH 符号化バイト値の16進数表現 (HH の16進数2文字で表現)
\cx, \C-x 制御文字 (x は a から z までのいずれかの文字)
\M-x メタ (x|0x80)
\M-\C-x メタ制御文字
\uHHHH ユニコード文字 (HHHH の16進数4桁)
\u{HHHHHH HHHHHH ....} ユニコード文字列 (HHHHHH は16進数1桁から6桁まで指定可能)\b は文字クラス内でのみ有効な表現です。文字クラスの外では 単語の区切りを表すメタ文字列と解釈されます。
「\s」は文字列では空白(0x20)を意味しますが、正規表現では タブなどを含む空白文字全般にマッチするメタ文字列です。
- 投稿日:2019-11-24T20:18:04+09:00
文字
正規表現内では、「\」の後に文字列を置くことで、 ある特定の文字を表現することができます。 これは、改行のように Ruby の文法で特別な意味を持つ文字を埋め込む ためなどに用いられます。 文字列リテラルの記法とほぼ同様(リテラル/バックスラッシュ記法)で、 以下の記法が利用可能です。
\t 水平タブ horizontal tab (0x09)
\v 垂直タブ vertical tab (0x0B)
\n 改行 newline (0x0A)
\r 復帰 return (0x0D)
\b バックスペース back space (0x08)
\f 改ページ form feed (0x0C)
\a ベル bell (0x07)
\e エスケープ文字 escape (0x1B)
\nnn 符号化バイト値の8進数表現 (nnn の8進数3文字で表現)
\xHH 符号化バイト値の16進数表現 (HH の16進数2文字で表現)
\cx, \C-x 制御文字 (x は a から z までのいずれかの文字)
\M-x メタ (x|0x80)
\M-\C-x メタ制御文字
\uHHHH ユニコード文字 (HHHH の16進数4桁)
\u{HHHHHH HHHHHH ....} ユニコード文字列 (HHHHHH は16進数1桁から6桁まで指定可能)
\b は文字クラス内でのみ有効な表現です。文字クラスの外では 単語の区切りを表すメタ文字列と解釈されます。「\s」は文字列では空白(0x20)を意味しますが、正規表現では タブなどを含む空白文字全般にマッチするメタ文字列です。
- 投稿日:2019-11-24T20:14:23+09:00
正規表現 式展開
正規表現内では、#{式} という形式で式を評価した文字列を埋め込むことが できます。
place = "東京都"
/#{place}/.match("Go to 東京都") # => #< MatchData "東京都">
埋め込んだ文字列にメタ文字が含まれているならば、それは メタ文字として認識されます。number = "(\d+)"
operator = "(\+|-|\*|/)"
/#{number}#{operator}#{number}/.match("43+291") # => #< MatchData "43+291" 1:"43" 2:"+" 3:"291">
埋め込む文字列をリテラルとして認識させたい場合は Regexp.quote を 使います。
- 投稿日:2019-11-24T20:14:23+09:00
式展開
正規表現内では、#{式} という形式で式を評価した文字列を埋め込むことが できます。
place = "東京都"
/#{place}/.match("Go to 東京都") # => #< MatchData "東京都">
埋め込んだ文字列にメタ文字が含まれているならば、それは メタ文字として認識されます。number = "(\d+)"
operator = "(\+|-|\*|/)"
/#{number}#{operator}#{number}/.match("43+291") # => #< MatchData "43+291" 1:"43" 2:"+" 3:"291">
埋め込む文字列をリテラルとして認識させたい場合は Regexp.quote を 使います。
- 投稿日:2019-11-24T20:10:12+09:00
正規表現 メタ文字列とリテラル、メタ文字とエスケープ
正規表現の文法には、正規表現内で特別な働きをする文字列と、それ以外の その文字列そのものにマッチするような文字列があります。 前者をメタ文字列(meta string)、後者をリテラル(文字列)(literal string)と呼びます。
/京都|大阪|神戸/
という正規表現においては、「京都」「大阪」「神戸」がリテラルで、 2つの「|」がメタ文字列です。以下の文字は「メタ文字」(meta character) と呼ばれる、正規表現内で特殊な働きをする文字です。
( ) [ ] { } . ? + * | \
これらの文字をリテラルのようにその文字としてマッチさせるためには、 バックスラッシュ「\」を前に付けます。「\」はバックスラッシュ1文字に マッチします。メタ文字以外の文字も、メタ文字に続けて置くことで特別な働きをするよう になる場合があります。つまりメタ文字列を構成します。例えば
/[a-z]/
/\Axyz\Z/
という正規表現において "[a-z]", "\A", "\Z"はメタ文字列です。
- 投稿日:2019-11-24T20:10:12+09:00
メタ文字列とリテラル、メタ文字とエスケープ
正規表現の文法には、正規表現内で特別な働きをする文字列と、それ以外の その文字列そのものにマッチするような文字列があります。 前者をメタ文字列(meta string)、後者をリテラル(文字列)(literal string)と呼びます。
/京都|大阪|神戸/
という正規表現においては、「京都」「大阪」「神戸」がリテラルで、 2つの「|」がメタ文字列です。以下の文字は「メタ文字」(meta character) と呼ばれる、正規表現内で特殊な働きをする文字です。
( ) [ ] { } . ? + * | \
これらの文字をリテラルのようにその文字としてマッチさせるためには、 バックスラッシュ「\」を前に付けます。「\」はバックスラッシュ1文字に マッチします。メタ文字以外の文字も、メタ文字に続けて置くことで特別な働きをするよう になる場合があります。つまりメタ文字列を構成します。例えば
/[a-z]/
/\Axyz\Z/
という正規表現において "[a-z]", "\A", "\Z"はメタ文字列です。
- 投稿日:2019-11-24T19:10:42+09:00
【Rails】ログイン機能を実装する
TODOアプリにログイン機能を実装します。
・【Rails】バリデーションを実装する
・【Rails】パーシャルを利用するUserモデルを作成
name
、password_digest
という属性を持つUserモデルを作成します。password
ではなく、必ずpassword_digest
という属性を設定してください。$ rails g model User name:string email:string password_digest:string
$ rails db:migrate
ハッシュ化したパスワードで認証ができるようにする
パスワードを平文で保存することは危険です。ハッシュ化したパスワードをDBに保存し、ユーザーのログイン時に入力されたパスワードをハッシュ化した値と比較するようにしましょう。
bcrypt
を追加ハッシュ化を行うためのgemを追加します。
/Gemfilesource 'https://rubygems.org' gem 'rails', '5.1.6' gem 'bcrypt', '3.1.12' . . .$ bundle installUserモデルに
has_secure_password
を追加
password_digest
という属性を持つモデルにhas_secure_password
と書き込むことで、以下の3つが可能になります。
- セキュアにハッシュ化したパスワードを、データベース内のpassword_digestという属性に保存できるようになる。
- 仮想的な属性
password
とpassword_confirmation
が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される。authenticateメソッド
が使えるようになる (引数の文字列がパスワードと一致するとUserオブジェクトを、間違っているとfalseを返すメソッド) 。/app/models/user.rbclass User < ApplicationRecord has_secure_password endログイン用のダミーデータを作成
password_digest
という属性がある場合、ユーザー登録時にはpassword
、password_confirmation
の2つの属性を登録します。$ rails c
irb(main):001:0> User.create(name: "sampleTarou", email: "tarou@example.com", password: "hogehoge", password_confirmation: "hogehoge")セッション管理の基盤を作成
セッションを利用してユーザーを判別する仕組みを作ります。
Sessionsコントローラを生成するときに(密かに)自動生成されるセッション用ヘルパーモジュール
を利用すると、sessionメソッド
が使えるようになり、セッション管理の仕組みを簡単に実装することができます。具体的には、ログイン時に
session[:user_id] = user.id
でブラウザのcookieにハッシュ化したユーザーidを保存し、ページ遷移の度に
@current_user = User.find_by(id: session[:user_id])
でidを元にログイン中のユーザーの情報を取得できるようになります。Sessionsコントローラーを作成
new
、create
、destroy
アクションを持ったSessionsコントローラーを作成します。
viewが必要なのはログインページを表示するアクションであるnew
アクションだけなので、余計なファイルを生成しないためにcreate
、destroy
アクションは手動で追加します。$ rails g controller Sessions new
/app/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new end def create end def destroy end endルーティングを設定します。
/config/routes.rbRails.application.routes.draw do get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' . . endSessionヘルパーモジュールを読み込む
sessionメソッド
を全てのページで使えるようにするために、Applicationコントローラ
にSessionヘルパーモジュール
を読み込みます。/app/controllers/application_controller.rbclass ApplicationController < ActionController::Base protect_from_forgery with: :exception include SessionsHelper endログインページを作成
セッションにはSessionモデルというものがなく、
@task
のようなインスタンス変数に相当するものもありません。本来
form_for(@task)
と書くだけで、「フォームのactionは/taskというURLへのPOSTである」と自動的に判定しますが、セッションの場合はリソースの名前とそれに対応するURLを具体的に指定する必要があります。/app/views/sessions.new.html.erb<h1>ログイン</h1> <%= form_for(:session, url: login_path) do |f| %> <%= f.label :email %> <%= f.email_field :email %> <%= f.label :password %> <%= f.password_field :password %> <%= f.submit "Log in" %> <% end %>ログイン状態に応じてトップページの表示を切り分け
ログイン前はログイン画面へのリンク、ログイン後はタスク一覧とログアウトボタンが表示されるようにします。(
logged_in?
メソッドについては後述)/app/views/tasks/index.html.erb<h1>TODOアプリ</h1> <% if logged_in? %> <%= render 'tasks/logged_in' %> <% else %> <%= render 'tasks/not_logged_in' %> <% end %>/app/views/tasks/_logged_in.html.erb<h2>タスク一覧</h2> <table> <thead> <tr> <th>タスク名</th> </tr> </thead> <tbody> <% @tasks.each do |task| %> <tr> <td><%= task.title %></td> <td><%= link_to "編集", edit_task_path(task) %></td> <td><%= link_to "削除", task_path(task), method: :delete %></td> </tr> <% end %> </tbody> </table> <%= link_to "タスク追加", new_task_path %> <%= link_to "ログアウト", logout_path, method: :delete %>/app/views/tasks/_not_logged_in.html.erb<p>タスクの管理ができるアプリです。まずはログインをしてください。</p> <%= link_to "ログイン", login_path%>セッション用ヘルパーメソッドを作成
log_in
メソッドブラウザのcookieに、ハッシュ化したユーザーidを保存するメソッドです。
app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end end
current_user
メソッドcookieに保存されたユーザーidを元に、ユーザーの情報を取得するメソッドです。
app/helpers/sessions_helper.rbmodule SessionsHelper . . # 現在ログイン中のユーザーを返す (いる場合) def current_user if session[:user_id] #@current_user = @current_user || User.find_by(id: session[:user_id])と同じ意味 @current_user ||= User.find_by(id: session[:user_id]) end end end
find
ではなくfind_by
で検索をしているのは、ユーザーidが存在しなかった場合find
を使うと例外が返されてしまうためです。find_by
を使うとnil
が返されます。
logged_in?
メソッド現在のユーザーがログインしているかどうかを判別するメソッドです。ログイン状況に応じて表示する画面を切り替えたりする処理が簡単に実装できるようになります。
app/helpers/sessions_helper.rbmodule SessionsHelper . . # ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? end end
log_out
メソッドブラウザのcookieに保存されているユーザーidを削除するメソッドです。
app/helpers/sessions_helper.rbmodule SessionsHelper . . # 現在のユーザーをログアウトする def log_out session.delete(:user_id) @current_user = nil end endログイン、ログアウト処理を実装
create
アクションを作成ログインページから送信された情報を受け取り、ログイン処理を行うアクションです。
app/controllers/sessions_controller.rbclass SessionsController < ApplicationController . . def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user redirect_to root_url else render 'new' end end . .
destroy
アクションを作成cookieに保存されたユーザーidを削除し、ログアウトを行うアクションです。
app/controllers/sessions_controller.rbclass SessionsController < ApplicationController . . def destroy log_out if logged_in? redirect_to root_url end endタスクをいじる前にログイン状況をチェック
タスクの表示、作成、編集、削除の前にログイン状況をチェックするように設定します。
logged_in_user
アクションを作成ログインしていないユーザーをログインページにリダイレクトするアクションを作成します。
application_controller
に追加することで、全てのコントローラーで利用できるようになります。/app/controllers/application_controller.rbclass ApplicationController < ActionController::Base . . private # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? redirect_to login_url end end end
before_action
を設定コントローラーの冒頭に
before_action :<privateアクション名>, only:[:アクション名]
と書き込むことで、アクションが実行される前に、指定したprivateアクションを実行することができます。/app/controllers/tasks_controller.rbclass TasksController < ApplicationController before_action :logged_in_user, only:[:create, :edit, :update, :destroy] . . .終わりに
予想以上に分量が多くなりました。今回実装した内容だと、ブラウザを閉じる度にセッションが切れてログアウトします。ブラウザを閉じてもセッションを維持する方法もあるので、またいつか他の記事でまとめたいと思います。
- 投稿日:2019-11-24T18:35:43+09:00
10進数でも2進数でも8進数でも逆から読んで同じになる回文数(Ruby)
回文とは
世の中ね、顔かお金かなのよ...
(よのなかねかお か おかねかなのよ)
のように上から読んでも下から読んでも同じ言葉を回文と言います。
10進数 2進数 8進数 0 0 0 1 1 1 2 10 2 3 11 3 4 100 4 5 101 5 6 110 6 7 111 7 8 1000 10 9 1001 11 10 1010 12 例えば、上の表では10進数で9が2進数、8進数共に回文数となっています。
今回は10以上の数で10進数、2進数、8進数共に回文となる最初の数を探してみます。
考え方
まず2進数で一番右が"0"の数を考えてみます。10101010とか。
回文でなければならないので、一番右が"0"だど一番左も"0"にならなければいけないのですが、そのような2進数は存在しません。(例えば、00101010は存在しない)
なので右も左も"1"から始まる2進数になるはずですが、これは必ず奇数になります。なので奇数だけ順番に探していけば良いことになります。10進数の回文数
ある10進数が回文数となっているかどうかは、reverseメソッドを使って、
qiita.rbnum.to_s == num.to_s.reverseを使えば分かります。
その10進数が2進数でも回文数になっているか
またある10進数の、その数の2進数が回文数になっているかどうかは、.to_sメソッドを使えば
qiita.rbnum.to_s(2) == num.to_s(2).reverseで分かるようになります。
to.s()のカッコの中に数字を入れると、その進数に変換することが出来ます。あとは10以上の10進数で2進数、8進数共に回文数になっている数をif文とwhileループで探すだけです!
qiita.rbnum = 11 border = "-------" while true if num.to_s == num.to_s.reverse && num.to_s(8) == num.to_s(8).reverse && num.to_s(2) == num.to_s(2).reverse puts num puts num.to_s(2) puts num.to_s(8) puts "we found it!" break end puts num puts num.to_s(2) puts num.to_s(8) puts "not this one." puts border num += 2 end計算してみると、
qiita.rb585 1001001001 1111になりました!
ちなみに
これに16進数でも回文数となるような数を探そうと思って、条件を付け足してプラグラムを走らせてみたのですが、何分待っても計算が終わりませんでした、、、
- 投稿日:2019-11-24T17:03:16+09:00
Railsのconfig.eager_loadの影響によるエラーについて
自身への注意喚起と同じところで躓いてしまった人がいたらすぐ解決できるように、メモします。
起こった問題
以下のようなcontrollerとviewがあります。(管理者一覧画面)
sysadmin_users_controller.rbclass SysadminUsersController < ApplicationController def index @sysadmin_users = SysadminUser.order(updated_at: :desc) end endindex.html.slim= "#{@sysadmin_users.count}件" table thead tr th | 氏名 th | メールアドレス th | 最終更新日 tbody - @sysadmin_users.each do |sysadmin_user| td = sysadmin_user.name td = sysadmin_user.email td = l(sysadmin_user.updated_at, format: :short)上記該当するURL(/sysadmin_users)にアクセスするとdevelopment環境では問題なかったのですが、staging環境では、index.html.slimの
count
がno metthod errorになりましたエラーの発生原因と解決方法
development環境とstaging環境によってエラーが発生するか違うため、環境によって異なる点を元にエラー原因を確かめようとしました。
1. データベースの違い
development環境とstaging環境で読み込んでいるデータベースが異なっているため、staging環境のデータベースをdevelopment環境にインポートし、同じテーブル状態にしました。
結果
エラー原因分からず、development環境はエラーが発生しませんでした。
2. configファイルの違い
環境ごとに異なるのが、app>config>environments配下にあるファイルです。
development.rbとstaging.rbで異なる箇所を探っていくと、怪しい箇所が・・・。development.rbconfig.eager_load = falsestaging.rbconfig.eager_load = truedevelopment.rbを
config.eager_load = true
に書き換えると・・・
staging環境と同じエラーが発生しました!config.eager_loadって何?
@shakemurasanさんの記事が分かりやすいと思いますので、こちらを参照してみてください。
https://qiita.com/shakemurasan/items/305bd3d78d67b646bc06config.eager_loadをtrueにすると、controllerやmodelなどのclass名がすべて事前に読み込まれます。(事前というのはおそらくrails serverが起動するタイミングです)
そのため、ページを読み込むたびにclassを読み込む必要はないので、ページ読み込みスピードは速くなります。
一方、config.eager_loadをfalseにすると、ページ読み込みスピードは遅くなりますが、rails serverの起動スピードは速くなります。ちなみに、development環境においてconfig.eager_loadをfalseにしている理由としては、stagingとは異なり頻繁にソースコードが変更され、その度にrails serverを再起動しdevelopment環境でブラウザテストしているという背景があるためです。
なぜconfig.eager_loadの設定でエラーが発生したのか?
事前にnamespace(class名)をすべて読み込まれるとエラーが起こって、ページにアクセスしたタイミングで特定のclassを読み込むとエラーが発生しない・・・。
ん?もしや同じnamespace(class名)が存在しているのでは?
SysadminUsersControllerで全文検索すると、ありました!同じnamespace(class名)が!users_controller.rbclass SysadminUsersController < ApplicationController def index end end衝撃的ことが2点。
1. users_contorllerなのに、class名がSysadminUsersController
2. このファイルは全く使用されていない・・・ファイルを消したら、無事解決しました
今後の対策
config.eager_loadについて理解するきっかけになったものの・・・。以下新たに肝に銘じておきます。
1. 環境ごとにエラーの有無が異なったら、app>config>environments配下にあるファイルを確認する。(まあこれはいいとして)
2. controller名とclass名を統一する
2. 使っていないファイル(controllerに限らず)は削除する(おそらくあとで使うかもしれないと残していたかもしれないが、エラーが発生する原因になり得る)最後に
今回のエラーの原因は開発経験半年越えている人にとってはとても当たり前のことかもしれませんが、初めたてのメンバーからすると当たり前ではありません。チームで開発していると今初めたてのメンバーがいなくても今後現れるはずなので、しっかりドキュメントに記載し、ルール化、注意喚起したいと思います
- 投稿日:2019-11-24T16:44:09+09:00
Rails 5 アプリのherokuへのデプロイ手順
環境
Rails 5.0.7.2
Ruby 2.6.3herokuへのデプロイ手順
- heroku にアプリを作成(すでにherokuにログインしていると仮定)
$ heroku create herokuアプリ名
- 本番環境用にGemfile, database.ymlを変更
- Gemfilm: Postgre SQLとRailsを連携させる
- database.yml: DBをPostgreSQLに設定
- 下記を追加,実行し,本番環境へ反映
- 本番環境上でアセットパイプラインを通るようにプリコンパイル処理を実行する
config/environments/production.rb config.assets.compile = false
- 本番環境上でアセットパイプラインを自動で通るように設定を変更する
$ rake assets:precompile RAILS_ENV=production
- 試したい場合は,以下コマンドを打ってエラーが出ないかを見てみる
$ RAILS_ENV=production bundle exec rake assets:precompile
- heroku にデプロイ
$ git push heroku master
- マイグレーション
$ heroku run rails db:migrate
詳細:本番環境用にGemfile, database.ymlを変更
group :production do gem 'pg', '0.21.0' enddatabase.yml略 production: adapter: postgresql encoding: unicode pool: 5 database: message-board_production username: message-board password: <%= ENV['MESSAGE-BOARD_DATABASE_PASSWORD'] %>まとめ
一番最初に(herokuへデプロイする前に)アセットパイプラインのための設定をしておきましょう.
私はこの順番をミスりました.
- 投稿日:2019-11-24T16:40:53+09:00
Ruby Net::SSHで多段ssh(ポートフォワード/トンネリング)をプログラマブルにやる
Goal
- sshをプログラマブルにやる
- 踏み台サーバ経由でsshする
- 秘密鍵をメモリ上におき、ディスクにおかなくていいようにする
- (おまけ)最後にspecinfra, itamaeをこの方式でつかえるようにする
Motivation
秘密鍵の管理がややこしい。案件が増えるにつれてどんどん増えていく。踏み台サーバの鍵と目的サーバの鍵が異なることもあり、サーバ構築時(ユーザ作成前)と構築後でつかう鍵が異なることもある。鍵を担当者に共有するのもややこしい。
少なくとも自分でコントロールできる案件については、構築に関係する鍵をプログラマブルに管理し、構築自体動的にやりたいと思うようになった。
Mechanism
key_data
ふつうコマンドラインのオプションでは
-i
など、鍵のパスを指定するのが一般的だが、Net::SSH
にはkey_data
というオプションで、文字列で直接秘密鍵を指定する仕組みがある。 1 これをつかえば、秘密鍵をディスク上に保存することなくsshできる。require 'net/ssh' key_data = <<-EOS -----BEGIN RSA PRIVATE KEY----- MIIEowxxxxxxxx..... -----END RSA PRIVATE KEY----- EOS Net::SSH.start('x.x.x.x', 'ec2-user', port: 22, key_data: key_data ) do |ssh| ssh.exec!("hostname") end # => "ip-y-y-y-y\n"踏み台サーバごしに
key_data
をつかえれば今回の要件をクリアできるという寸法。ProxyCommand
capistranoで踏み台経由のデプロイするときは、今までProxyCommandをつかっていた。capistranoは裏でNet::SSHをつかってるので、
Net::SSH::Proxy::Command
をつかってプロキシできる。今回の多段sshも、はじめこれと同じ要領でできるような気がしていたが、どうもできない。
net-ssh/command.rb at v5.2.0 · net-ssh/net-ssh · GitHub
Net::SSH::Proxy::Command
は引数に渡されたコマンドを、OSコマンドライン上で実行しているにすぎない(IO.popen
)。つまりNet::SSH
がもっているプログラマブルな鍵管理の仕組み(key_data
など)を適用できない。しかしコマンドラインのsshでは秘密鍵を直接文字列で渡す仕組みはない。(コマンドライン実行されたものは履歴に残るので仮にできたとしてもやるべきではない)require 'net/ssh' require 'net/ssh/proxy/command' key_data = <<-EOS -----BEGIN RSA PRIVATE KEY----- MIIEowxxxxxxxx..... -----END RSA PRIVATE KEY----- EOS Net::SSH.start('x.x.x.x', 'ec2-user', port: 22, key_data: key_data, # ↓のコマンドに踏み台サーバログインのための鍵のパスを指定する必要がある # 踏み台へのアクセス時にはkey_dataに指定した鍵はつかわれない proxy: Net::SSH::Proxy::Command.new('ssh -W %h:%p metheglin@a.a.a.a -i ~/.ssh/id_rsa'), ) do |ssh| ssh.exec!("hostname") end # => "ip-y-y-y-y\n"つまり、ProxyCommandつかうなら必ず秘密鍵がファイルとしてディスク上に配置されていなければいけない。今回の要件ではProxyCommandはつかえないことになる。
net-ssh-gateway
じつはわたしは、多段sshについて理解がとぼしく、軽く混乱状態にあった。ProxyCommandができないとわかると混乱きわまってあきらめかけた。
一度冷静になって、できないわけないと思い直し、歯を食いしばって調べ直した結果以下のgemを見つけた。最近歯医者に行ったからくいしばり力があがってたのかもしれない。READMEにはactiveに開発されてないとコメントがあるが、中身はすごいシンプルで、214行くらいしかない。2 このgemは、シンプルにローカルフォワード3をおこなっている。適当なポートがはらいだされて4、それがフォワードの窓口となる。プログラマはそのポートについて意識しなくてもいい。
Sample Code
net-ssh-gatewayをつかって、秘密鍵をディスクにおかない多段sshは以下のようにできる。
gem install net-ssh-gateway
require 'net/ssh/gateway' key_data = <<-EOS -----BEGIN RSA PRIVATE KEY----- MIIEowxxxxxxxxxxxx... -----END RSA PRIVATE KEY----- EOS gateway = Net::SSH::Gateway.new('x.x.x.x', 'ec2-user', key_data: key_data) ssh = gateway.ssh('a.a.a.a', 'ec2-user', key_data: key_data ) ssh.exec!("hostname; pwd; id") ssh.close gateway.close(ssh.transport.port)
Net::SSH::Gateway.new
した時点で、踏み台への接続が確立される。
その後gateway.ssh()
するともういっこ接続が確立される。ここでは踏み台サーバとターゲットサーバで同じ
key_data
をつかっているが、別の鍵にすることも可能。specinfra
specinfraでこのgatewayごしのssh接続をどうつかうか?
specinfraではハックポイントが用意されていて、カスタムのssh接続をつかえるようになっていた。5
上記のコードgateway.ssh()
でつくった接続ssh
を以下のように指定すればOK。specinfra = Specinfra::Backend::Ssh.new( request_pty: true, disable_sudo: false, ssh: ssh, ssh_options: { user: 'ec2-user', } ) specinfra.run_command("hostname; pwd; id") # ssh.close # gateway.close(ssh.transport.port)itamae
itamaeはコマンドライン上の実行を主に想定してるっぽいので、大胆なハックができない。
以下のようにプログラマブルで実行用のRunnerをつくることでしのぐ。
上記specinfraのコードで作成した変数specinfra
を指定してつかう。recipe = File.expand_path("recipes/ping.rb", __dir__) Itamae::InlineRunner.run( [recipe], specinfra: specinfra, node: {test: 1234}, )module Itamae class InlineRunner < Itamae::Runner class << self def run(recipe_files, options) Itamae.logger.info "Starting Itamae... #{options[:dry_run] ? '(dry-run)' : ''}" backend = Backend::SshInline.new(options) runner = self.new(backend, options) runner.load_recipes(recipe_files) runner.run runner end end private def create_node return @options[:node] if @options[:node] super end end module Backend class SshInline < Base def initialize(options) @options = options @backend = options[:specinfra] @executed_commands = [] end def disable_sudo? !@options[:sudo] end end end endEnvironment
% ruby -v ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux][2] pry(main)> Net::SSH::Version::STRING => "5.2.0" [3] pry(main)> Net::SSH::Gateway::VERSION => "2.0.0" [4] pry(main)> Specinfra::VERSION => "2.82.4" [5] pry(main)> Itamae::VERSION => "1.10.6"
net-ssh-gateway/gateway.rb at v1.2.0 · net-ssh/net-ssh-gateway · GitHub ↩
net-ssh-gateway/gateway.rb at v1.2.0 · net-ssh/net-ssh-gateway · GitHub (自動的にポートの払い出しがおこなわれるが、番号を明示指定することもできる。) ↩
- 投稿日:2019-11-24T16:33:25+09:00
Rails5 heroku にS3を使ってアップロードしたときに発生するエラー 403 Access denied の対処法
背景
S3にあるリソースはデフォルトでは外部からアクセスできないようになっている.
Heroku上で動いているアプリで画像をアップロードしても,403 errorではじかれて,表示できない.やったこと
パブリックアクセス設定を一度すべてオフにする
バケットポリシーを作成
アクセス権限→バケットポリシーを選択すると,編集画面になるので,以下の内容を書く.
{ "Version": "2012-10-17", "Id": "PublicRead", "Statement": [ { "Sid": "ReadAccess", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::<あなたのAWS12桁のID>:user/<IAMユーザー名>" }, "Action": "*", "Resource": "arn:aws:s3:::<bucketの名前>/*" } ] }パブリックアクセス設定を変えて,新しいバケットポリシーをブロックさせる
参考
https://qiita.com/rainbow___0/items/0ffeb29afc7865eb65b5
https://teratail.com/questions/170083
- 投稿日:2019-11-24T12:37:19+09:00
【RSpec3.9】苦手意識克服!一番シンプルな文法で簡単なテストを書いて理解してみよう
はじめに
これからRSpecを導入しようとされている方の中で、
- テストに対する抵抗感をなくしたい
- まずは簡単なテストの書き方を知りたい
という方向けに書きました。
RSpecの超基本文法とそれを使ったテストの例を以下4つの視点で1つずつ掲載しています。
- Model
- Controller
- View
- Routing
参考になれば幸いです。
【参考にさせて頂いた記事】
使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita
使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita
使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita
Ruby on Rails のテストフレームワーク RSpec 事始め - Qiitaこの記事が役に立つ方
- RSpecでテストを書いたことがない方
この記事のメリット
- RSpecへの抵抗感がなくなる
環境
- OS: macOS Catalina 10.15.1
- シェル: zsh
- Ruby: 2.6.5
- Rails: 5.2.3
- RSpec: 3.9.0
【事前準備】
今回はscaffoldを使ったアプリケーションを例にしますので、3ステップで事前準備をしておきましょう。
1. scaffoldでアプリケーションを作成しておく
$ rails g scaffold user name:string email:string
2. RSpecをインストールし、テスト用ファイルを作成する
Gemfilegroup :development, :test do gem `rspec-rails` end↓
$ bundle install↓
$ rails g rspec:install
3.テストの実行方法の把握
$ rspec
上記で全テストが走りますので、テストを書いたら走らせてみましょう。
次に基本文法を見ていきます。
基本文法
require 'rails_helper' RSpec.describe テスト対象, テストの種類 do describe 'テスト対象' do it '期待する内容を言葉で書く' do 期待する動作をコードで書く end end end※実際は他に
context
、before
なども使いますが、本記事では超シンプルなテストを例にします。基本文法だけ見てもよく分からないと思いますので、さっと目を通して以下具体例に移りましょう。
具体例
MVC+Routingそれぞれに対し、1つずつ超シンプルなテストを書いて試していきます。
それぞれ、動作するテストのみ記載していますので、自分でいじってみて失敗するかどうかも確認するほうが良いと思います。
1.Model
バリデーションが正しく動作するかをテストしたい
Userモデルに以下のようなバリデーションがかかっていて、それをテストしたいとします。
models/user.rbclass User < ApplicationRecord with_options presence: true do validates :name validates :email end endでは、テストを書いてみましょう。
最初に以下コマンドを実行し、テストの雛形を作成しましょう。$ rails g rspec:models user
spec/models/user_spec.rb
が出来ているはずなので、以下のように記述して下さい。spec/models/user_spec.rbrequire 'rails_helper' RSpec.describe User, type: :model do describe 'Userモデルのバリデーションテスト' do it 'nameとemailに値があれば有効' do user = User.new( name: "name", email: "sample@example.com" ) expect(user).to be_valid end end end【解説】
User, type: :model
でUser Modelのテストであることを明示。
user
に適当な名前とメールアドレスを指定してUserモデルから新規インスタンスを作成し、それをbe_valid
というマッチャで有効かどうかを判定しています。
2.Controller
indexアクションで正常にページが表示されるかテストしたい
正常にページが表示されるかはHTTPレスポンスが200であると言い換えることが出来ます。
テストを書いてみましょう。
まず先程と同様、以下コマンドを実行します。$ rails g rspec:request users「あれ?request? controllerじゃないの?」と思いますよね。
実は、Rails5以降はcontroller_specが非推奨となっています。
controller_specでも書けるのですが、controller関連はrequest_specで記載するのが良いようです。
詳細は下記リンクより。では戻ります。
先程のコマンドで
spec/request/users_spec.rb
が出来ているので以下のように記述して下さい。spec/request/users_spec.rbrequire 'rails_helper' RSpec.describe "Users", type: :request do describe "GET /users" do it "HTTPレスポンスが200になる" do get users_path expect(response).to have_http_status(200) end end end【解説】
"Users", type: :request
と指定することでUsersのrequest_specであることを明示。getリクエストで
users_path
(/users)を指定し、それに対するHTTPレスポンスがresponse
に入ります。その
response
に対してhave_http_status
というマッチャで200
と等しいかを確認しています。
3.View
<h1>
タグ内が正しく表示されるかをテストするapp/views/users/index.html.erb<h1>Users</h1>
index/html/erb
にはこんなh1
タグが記載されています。この
h1
タグ内に表示されている内容が正しく表示されているかをテストしてみましょう。まずはテスト用のファイルを以下のように作成します。
$ rails g rspec:view users index
spec/views/users/index.html.erb_spec.rb
が出来ているので、以下のように記述して下さい。spec/views/users/index.html.erb_spec.rbrequire 'rails_helper' RSpec.describe "users/index", type: :view do describe 'index.html.erbのテスト' do it 'h1タグ内にUsersが表示されているかどうか' do visit users_path expect(page).to have_selector('h1', text: 'Users') end end end【解説】
users/index
のview
のテストであることを最初に明示。
visit
の後にusers_path
を指定することでindex.html.erb
にアクセス。
expect(page)
で今いるページを明示、have_selector
というマッチャで'h1'にUsers
が表示されているかをテストしています。
...※実は上記テストはうまくいきません。
騙したようで申し訳ありません。テストを実行したら、
NoMethodError: undefined method 'visit'
というエラーが出ませんでしたか?
実は自分もここで手間取りました。ググるとかなり沢山記事が出てくるので、良くあるエラーなんだと思います。
原因はgem
capybara
がRSpecに読み込まれていないことです。
visit
はcapybara
の中で定義されているDSL(ドメイン指定言語)のため、読み込むために設定を追加しなければいけません。
spec/spec_helper.rb
を開いて以下のように2行追記して下さい。spec/spec_helper.rb...略 require 'capybara/rspec' # ここに追記 RSpec.configure do |config| config.include Capybara::DSL # ここに追記 ...略 endこの設定で読み込まれる
Capybara::DSL
で定義されているDSL一覧は以下で確認することができます。もし他に
NoMethodError
が出たら参考にしてみるのも良いかもしれません。うまくいったら次に進みましょう。
4.Routing
ページのルーティングが正しいかをテストする
最後はルーティングです。
createアクションに対してルーティングがうまくいっているかテストをしてみましょう。
テスト用のファイルはジェネレータを使わなくても作成出来るので、
今回は、手入力でファイルを作成します。
spec/routing/users_routing_spec.rb
を新規作成し、以下のように記述して下さい。spec/routing/users_routing_spec.rbrequire 'rails_helper' RSpec.describe "routes for Users", type: :routing do describe 'ルーティングのテスト' do it 'createアクションのルーティング' do expect(post("/users")).to route_to("users#create") end end end【解説】
Users
のrouting
についてのテストであることを明示。
/users
にPOSTしたらusers
コントローラーのcreate
アクションが動くことをテストしています。おわりに
いかがでしたでしょうか?
超基本的な内容ではありますが、書くことに対しての苦手意識が少しで減ればいいなと思います。
私も書きながら調べていましたが、検索でヒットする内容には古い情報が多く、苦労しました。
今後も以下最新情報を参照しつつ勉強していこうと思います
RSpec Rails 3-9 - RSpec Rails - RSpec - Relish参考にさせて頂いたサイト(いつもありがとうございます)
使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita
使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita
Ruby on Rails のテストフレームワーク RSpec 事始め - Qiita
全国のSeleniumer必読 - Qiita
undefined method `visit’ when using RSpec and Capybara in rails
capybara cheat sheet · GitHub
- 投稿日:2019-11-24T12:35:02+09:00
RSpecに画像ファイルアップロードのテストを通す
FactoryBotとCarrierWaveを使ってRSpecに画像ファイルアップロードのテストを通す
備忘録
FactoryBotとCarrierWaveを使ってRSpecに画像ファイルアップロードのテストを通す方法
環境
ruby 2.6.3
rails 5.2.3
carrierwave
FactoryBot-rails1.spec内にfixturesディレクトリを作成
2.model内のimageuplorderの記述があるか確認
3.FactoryBot内でpictureアップローダを呼び出す
factories/feed.rbFactoryBot.define do factory :feed do title { 'タイトル' } content { '投稿内容'} picture { Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/fixtures/rspec_test.png')) } user end end参考にしたサイト
- 投稿日:2019-11-24T12:01:34+09:00
【初心者】ターミナルとお友達になってみる
ターミナルと仲良くしたい
プログラム初心者がターミナルと仲良くなるために、
ご挨拶(命令)をまとめてみました。ターミナルでの基本命令
pwd
$ pwdprint working directoryの略。
現在のディレクトリの表示を行う為の命令cd
$ cdchange directtryの略。
ディレクトリ間を移動したい時に使う命令。ls
$ lslist の略。
現在のディレクトリにいるファイルを一覧で表示してくれる。exit
$ exitサーバーとの接続を切断するときに使う。
パソコンの電源をポチっと落としてシャットダウンするイメージです。clear
$ clearターミナルに表示されているテキストを綺麗さっぱりリセットをかける。
スッと消えると気持ちが良いです。irb
$ irbInteractive Ruby の略。
Rubyを使用する場合に使い、
主にRubyのコードの動作を確認する為に利用を行う。rails g controller posts
$ rails g controller postsRailsのコントローラーを新たに設置する命令。
これを行う事でmodelとviewの処理を繋ぐ事ができる。rails g model post
$ rails g model postRailsのモデルを新たに設置する命令。
これを行う事でデータベースへのアクセスへを処理を
行う箇所を設置する。rails db:migrate
$ rails db:migrateRailsでマイグレーションファイルを生成する命令
rails db:rollback
$ rails db:rollbackRailsでマイグレーションファイルをロールバックさせる命令
rails db:migrate:status
$ rails db:migrate:statusマイグレーションファイルがデータベースに反映されているかチェックする命令
- UP…適用されている
- DOWN…適応がされていない状態の為、そのまま修正や削除が可能。
覚えておくとちょっと幸せになる豆知識
自動補完(頭文字を売ってtabキー
これは結構便利だなと思ったんですが、
例えば「test」というファイルを指定したい場合、
「t」まで打ってtabキーを押すと残りの文字を自動補完してくれます。(ターミナル頭良い!!)
ただし同じディレクトリ内に「test」「template」みたいに同じ頭文字が並んだ場「te」で
表示が止まってしまいます。
これは「同じ頭文字2つあるけどどっちやねん」ってターミナルが聞いてくれているので、
「tes」か「tem」まで打てば各々を表示してくれます。
短いファイル名だとあまり恩恵は少ないですが、長いファイル名の正確な綴りって
うろ覚えの場合が多いので助かっております。
- 投稿日:2019-11-24T07:49:06+09:00
case文で条件分岐
使い方
対象の値が条件に合うかどうか調べます。
合っていたら処理を実行します。case 対象 when 条件1 条件1 when 条件2 処理2 ... else 処理3 end例1
fruits = banana case fruits when "apple" puts "It's red!" when "banana" puts "It's yellow" when "orange" puts "It's orange" else puts "I don't know" end結果
It's yellow
例2
score = 65 case score when 90..100 puts "A+" when 80..89 puts "A" when 70..79 puts "B" when 60..69 puts "C" else puts "F" end結果
C
if文との使い分け
条件式といえば if文 が真っ先に思いつきます。
上の例も if文 で書き換えることも可能です。では、どう使い分けるか。。。
基本 → if
条件式が複数あり、対象がすべて同じ場合 → case
僕はこうしてます。
caseを使ったほうが可読性は上がります。より良い意見あったらコメントお願いします。
ではまた!