20200206のRubyに関する記事は20件です。

【Ruby】scanとmatchの違い

scanとmatchの違いってなんなんだろう

Rubyの入門書のコード例を実行する中で、ある文字列から正規表現にマッチした文字列を取得する方法としてscanとmatchというメソッドを学びました。
両者は同じようにある文字列から正規表現にマッチする文字列返すというメソッドですが、入門書の中でもこの二つのメソッドは度々登場しているのですが、どのように使い分けたらよいのかわからなくなってしまいました。そこで、今回はscanとmatchの違いをまとめてみました。

※この記事ではMacにインストールされたRuby 2.6.3を使用しています。
実行環境はRubyファイルをターミナルで実行しています。

実際にscanとmatchを実行してみる

matchメソッドを実行してみる

実際に、"123-4567-89"という文字列から数字だけを正規表現を用いてmatchメソッドで取得してみます。

p '123-4567-89'.match(/(\d+)-(\d+)-(\d+)/)
#=> #<MatchData "123-4567-89" 1:"123" 2:"4567" 3:"89">

scanメソッドを実行してみる

p '123-4567-89'.scan(/(\d+)-(\d+)-(\d+)/)
#=> [["123", "4567", "89"]]

両者の比較

まず、matchメソッドで実行した場合は以下のように、マッチした文字列をMatchDataオブジェクトの形式で返していることがわかります。
Ruby2.6の公式ドキュメントにも、「指定された文字列 str に対して位置 pos から自身が表す正規表現によるマッチングを行います。マッチした場合には結果を MatchData オブジェクトで返します。マッチしなかった場合 nil を返します。」と説明があります。
引用元: https://docs.ruby-lang.org/ja/2.6.0/class/Regexp.html

#<MatchData "123-4567-89" 1:"123" 2:"4567" 3:"89">

また、scanメソッドで実行した場合は以下のように、マッチした文字列を配列として返していることがわかります。
Ruby2.6の公式ドキュメントにも、「マッチした部分文字列の配列を返します。」と説明があります。
引用元: https://docs.ruby-lang.org/ja/2.6.0/class/String.html

#[["123", "4567", "89"]]

以上から、matchメソッドはMatchDataオブジェクトの形式で、scanメソッドは配列で返しているという違いがあることがわかりました。

matchはRegexpクラスのインスタンスメソッド、scanはStringクラスのインスタンスメソッドであるという違いがあります。

matchメソッドの実行結果を配列で返す

scanメソッドのように実行結果を配列で返すにはvalues_atメソッドを使用します。values_atメソッドは引数にキーに対応する値指定することで、指定したキーに対応する値を配列として返すというメソッドです。

'123-4567-89'.match(/(\d+)-(\d+)-(\d+)/).values_at(1, 2, 3)
#=> ["123", "4567", "89"]

ちなみに、scanメソッドの実行結果が正規表現の中で()を使っているため配列の中の配列として返していますが、splat展開することで上の結果と同じ形で配列で返すことができます。

*'123-4567-89'.scan(/(\d+)-(\d+)-(\d+)/)
#=> ["123", "4567", "89"]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】DBのカラムにインデックスを付与する/しない場合での速度比較をしてみた

はじめに

RailsでDBにインデックスを付与するメリットを体感するために、試験的に1万件のレコードを作成し、インデックスの有無で実行速度の比較をしてみました。

今回のケースではおよそ18%の速度UPという結果が出ています:point_up:
※サクッと作成するため、DBはRailsのデフォルトであるSQLite3を使用しました。

2020/02/07追記
10万件のレコードだと20倍の差が出ていることを確認し、結果を追記しました。
@error_401 さんありがとうございます。

環境

OS: macOS Catalina 10.15.3
zsh: 5.7.1
Ruby: 2.6.5
Rails: 6.0.2.1

事前準備

rails_newする
$ rails new sampleapp
sampleappディレクトリに移動
$ cd sampleapp
db_createする
$ rails db:create
Postモデルの作成
$ rails g model Post name:string description:string
db_migrateする
$ rails db:migrate
事前に1万件のデータを登録
irb(main):001:0> 10000.times do |p|
  Post.create(name: "sample#{p}", description: "this is sample!#{p}")
end

準備が整ったら、実験開始です!

実行するコマンド

nameカラムを検索対象にし、find_byメソッドでランダムな名前のレコードを1万回変数postに代入するという内容です。

test.rbを作成します。

sampleapp/test.rb
require 'benchmark'

num_iteration = 10000

Benchmark.bm 10 do |r|
  r.report "no-index" do
    num_iteration.times do
      post = Post.find_by(name: "sample#{rand(1..1000)}")
    end
  end
end

1.インデックスなしの場合

rails_consoleで実行
irb(main):001:0> require './test.rb'
インデックスなし
       user     system      total        real
  2.400283   0.353093   2.753376 (  2.840555) # 1回目
  2.456253   0.352958   2.809211 (  2.914810) # 2回目
  2.527288   0.383071   2.910359 (  2.999960) # 3回目

測定の対象はuserの数値にします。

3回の平均はおよそ2.46秒でした。

2.インデックスありの場合

ターミナル
$ rails g migration add_index_to_post
XXXXXXXXXXXXXXXX_add_index_to_post.rb
class AddIndexToPost < ActiveRecord::Migration[6.0]
  def up
    add_index :posts, :name
  end
  def down
    remove_index :posts, :name
  end
end
ターミナル
$ rails db:migrate

これでインデックスがnameカラムに付与されたので、効果測定です。

再度test.rbを実行します。

rails_consoleで実行
irb(main):001:0> require './test.rb'
レコードあり
       user     system      total        real
  2.016621   0.349745   2.366366 (  2.455184) # 1回目
  2.037209   0.333974   2.371183 (  2.465041) # 2回目
  2.039167   0.327804   2.366971 (  2.471504) # 3回目

3回の平均はおよそ2.03秒

結論:およそ18%の速度UP

今回の単純なケースでは、およそ18%検索速度が上がりました!

もちろんtest.rbの内容によって異なることもあると思いますが、指標の一つとして使えるのではないかと思います。

以上です!

2020/02/07追記 レコード10万件の場合

@error_401 さんよりコメントを頂き、早速レコードを増やした場合にどうなるか検証してみました!

検索条件の変化

  • レコード数:10万件
  • 検索条件:nameカラムの検索条件をsample1sample100000へ変更。(回数は1万回で変更なし)

結果:インデックスなし

インデックスなし
       user     system      total        real
  39.969663  14.484736  54.454399 ( 54.855391) # 1回目
  40.167824  14.695771  54.863595 ( 55.215927) # 2回目
  40.057479  14.542640  54.600119 ( 54.955958) # 3回目

3回の平均はおよそ40.05秒

結果:インデックスあり

インデックスあり
       user     system      total        real
  2.007433   0.391915   2.399348 (  2.546632) # 1回目
  1.989761   0.376209   2.365970 (  2.450146) # 2回目
  2.029753   0.386575   2.416328 (  2.566732) # 3回目

3回の平均はおよそ2.00秒

10万件のレコード数だとなんと20倍速度が早くなりました!

ビックリしたのは、インデックスありだと1万件のときとほとんど数字が変わらないこと。
これはすごい:dizzy_face:

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

どなたかの参考になれば幸いです:relaxed:

参考にさせて頂いたサイト(いつもありがとうございます)

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

中規模以上レガシーRailsシステムのリファクタリング方針

前置き

ここ一年、現職において0→1フェーズからすくすく育ってきたRailsプロジェクトのバックエンド全般を任されており、リファクタリング方針において、ある程度いい感触を掴みつつあるので、その知見を共有してみようってことで書いてみました。

現状

  • 一連のビジネス要求に対する判断や分岐、データアクセスといったドメインロジックが、controllerや、方々のmodelに跨って定義されていることによるドメインロジックの見通しの悪さ
  • データアクセスと判断や分岐といった処理がmodelに一緒くたに実装されているため、テストコードの実装が難解である
  • テストコードの実装が難解であるため、テストの実装を諦め、人力動作確認でリリースまで進めるが、高確率で予期せぬデグレが発生する
  • リリース後にデグレ対応に追われるため、スムーズに次の開発へと移行できない

どうしよう
dousiyou.png

考察

テストがないことに起因した既存実装の考慮漏れによるリリース後のデグレに一番時間が割かれてしまうため、まずはテストコードを書こうと思い辺りました。
ただ、既存の実装のままだと、そのテストコードが書きづらいため、テストを実装していく前に、アプリケーションコードの設計改善が必要だと感じ、今のプロダクトの規模に合わせて、中規模以上でのRailsシステムにおけるテストが書きやすい設計を模索していく必要があります。

設計改善

ここでは具体的にどう改善していったのか(改善していこうとしているのか)を説明していきます。

現状は基本的には説明しやすくするために Skinny(かもしれない?) Controller, Fat Model の基本的なRails wayに則って実装されているとします。

それを以下の図のようなアーキテクチャへと変更していきました(一部まで適用できていない願望も含まれています)。
構成図-アプリケーション設計 (1).png

基本的には上図の通りに特に変更が発生しやすかったり、ビジネス的に重要度の高い部分からこちらの設計に実装を改善している段階です。

各層の役割

ここからは各層の役割について説明していきます。
上の図からなんとなく察している方もいらっしゃると思いますが、クリーンアーキテクチャの思想をRailsに合わせた形で表現していこうと試みています。

Presentation層

Controller

ここは、PCやスマホの画面といった各Clientからのリクエストを受け取り、後述するusecaseにデータを渡します。
usecaseにデータを渡し、usecase以降で行われた処理の結果をレスポンスとしてて返却することと、渡されてきたデータのバリデーションにのみ責任を負ってもらうようにしています。

Domain層

ビジネス要求にたいする判断や分岐のロジックを実装している層です。

usecase

controllerから渡されてきた値に対し、domainとdaoを組み合わせてビジネスロジックを表現しています。
ここで一番注意したいのは、判断や分岐、データアクセスといった処理は調節定義せず、
判断や分岐→domainで定義
データアクセス→daoで定義
といったように処理の実態はそれぞれ別の層で定義することを鉄則としています。

なぜそうしたを説明しますと、usecaseで判断や分岐、データアクセスを一緒くたに実装してしまうと、今までmodelに定義されていた、データアクセスと密結合になってしまったビジネスロジックの実装箇所が、ただmodelからusecaseに移動されただけで、データアクセスと密結合になっているが故に、テストを書くことの困難さの解決にならなくなってしまうからです。

domain

ここにビジネス要求における判断や分岐といった、データアクセスを伴わないビジネスロジックをピュアなRubyのクラスとして実装していきます。
もちろん、手続き型ではなく、OOPによる再利用性や、変更容易性といったメリットを享受した形での実装が行っていくため、今後システムとして質の良い実装状態を担保していくためには、ここでの実装力の勝負になってくるかと思っています。

dao

DAOとは(Data Access Object)の略です。
ビジネス要求に基づく、データ(主にRDB)への参照や、永続化を表現していく層になります。

データの参照や永続化は、modelでも定義してくことが出来るのですが、modelはRDBのテーブルと1対1で紐づくため、システムの中心となるmaster系のテーブルに紐づくmodelがデータの参照や更新のみでみるみる太っていくことが予想されるため、こういった層に切り出そうといった考えに落ち着くました。

では、modelは何をすればいいのかと言いますと、ActiveRecordの場合、リレーションを定義しないと、複数テーブルをjoinするようなデータ参照が行えないため、各modelへのリレーションの定義と、そのmodel単体で済むデータアクセスであればmodelに持たせてもいいのかな? と思っています。

Data層

model

上述した通り、各modelへのリレーションの定義と、そのそのmodel単体で済むデータアクセスのみを表現させます。

other data source

外部で持つデータとのやり取りをこの層に閉じ込めます。
よくある部分としては、DynamoDBやRedisといったデータソースとのやり取りがあるかと思います。
後は、非同期で処理させたいjobの起動や、イベント駆動での連携があるシステムでは、イベントの発火等もこの層に閉じ込められるかと思います。

今後の課題

現在はまだまだ既存の辛いままの実装が多く残っているのですが、全ての作業を止めてこの改善にだけ時間を費やすことはできないので、一番故障率が高く、ビジネス的にも重要度の高いところから順次適用を進めています。
インパクトの大きい部分から適用を進め、変更コストが下がり、デグレの発生頻度も下がってきた暁には、そこで空いたリソースを費やして、改善活動を加速していけるのではと考えています。
後、割とリファクタしたと思っても、既存実装の考慮漏れがあって事故ります。
変更しやすく実装し直しているはずなので、事故っても安易に切り戻しに走らず、例え一旦は切り戻したとしても、事故った部分を考慮した変更をリファクタ先の実装に加えていけばよい、加えていくべきだと思っています。
また、改善スピードを増すためには、同じように改善が行えるメンバーの育成についても考えていかなくていけないなと感じています。

最後に

本来であれば、中規模以上に成長したシステムは、Railsから卒業して中長期での開発手法であるDDD等と親和性の高い言語やFWに乗り換えていくべきだと思うのですが、スタートアップのような、小さな組織で効率よく開発を進めたいといったニーズと、小さいサービスを最大速度で実装できるためのRailsの犠牲的アーキテクチャの親和性が高く、そのままプロダクトも当たって成長してきたが、リプレースのためのリソースが取れず、Railsで保守や追加開発をせざるを得ないといった組織は、少なからず存在していると思います。
今回のリファクタリング方針が、そういったエンジニア達の助けに少しでも貢献できれば幸いです。

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

【Rails】railsブログサイトの練習で躓いたところメモ

railsチュートリアル+αでブログサイトを作成していて躓いたところをまとめました。

環境

  • ruby on rails
  • mysql
  • redis
  • docker
  • macOS

作ってみて

railsのブログアプリを作ること自体はそこまで難しいものではありませんでした。
ただデータベース周りがややこしくてそこにかなり時間をとられてしまったことが今回の反省点です。

Rails deviceを使ってユーザー関連機能を作る

ブログアプリにまず必要なのはユーザー関連のあれこれです。Railsはその辺かなり楽に作れるdeviceというgemがあります。

deviceを利用するとログイン認証やアクセス制限などが簡単に実装出来ます。今まで練習も兼ねてユーザー関連は自分で作ってたのですが、一度こういった機能を知ってしまうと戻れなくなってしまいますね。

ここはそこまで詰まらなかったので以下の参考サイトを見れば大丈夫だと思います。

view関連なども「Rails device view」とかで調べたらたくさん出てきます。

https://qiita.com/Hal_mai/items/350c400e8763ce0487a3】

https://www.pikawaka.com/rails/devise】

https://qiita.com/Hal_mai/items/350c400e8763ce0487a3】

Rails scaffoldを使って投稿機能を作る

投稿機能はRailsのscaffoldが便利です。勝手にMVCを作ってくれます。

これも以下の記事を見たら出来るので割愛します。

https://techacademy.jp/magazine/7204】

注意点

ここで1つ注意点と言いますか、知っておいた方が良いなと個人的に思ったことを書きます。

先にあげたdeviceとscaffoldは大変便利な代物で、私みたいな初心者でも簡単に実装できてしまうものなのですが、これらはあくまで時短や効率化のためにあるものだと思うので初心者が最初から多用するのは危ないと思いました。

私はこのブログ練習サイトの前に3つほどscaffoldとdeviceなしで1から似たようなものを作って練習しているのですが、その練習がなかったらいきなりこれを使っても結局何をしてくれてるのか分からずじまいだったと思います。

分からないのに実装出来ちゃうからわかった気になってしまう危険性があるので、まずはMVCの理解を深めるためにもプロゲートやドットインストールなどの基本的なサイトを見て仕組みをなんとなくでも理解しながら進めていくのがいいのではないかと思いました。

現在のユーザーとアクセス制限

current_user

deviceにはオプションとしてcurrent_user(現在のユーザー)なるものがついています。

例えば「投稿者が他の投稿者の記事の編集や削除を出来ないようにする」といった時に便利な機能です。

今回は投稿の編集と削除を投稿者以外できないようにしたかったので、posts_controller.rbに以下のように記述しました。

posts_controller.rb
before_action :ensure_correct_user, only: [:edit, :update, :destroy]

省略

private

省略

def ensure_correct_user
      if current_user.id!=@post.user.id
        flash[:notice]="Not yours"
        redirect_to(posts_path)
      end
end

private以下でensure_correct_userを定義して、before_actionでcurrent_user以外が使えないようにアクションに対して適応させています。

@post.user.idは投稿者のユーザーidのことです。これとcurrent_user.idが違ったら編集(update,edit)も削除(destroy)もできないですよって感じです。

user_signed_in?

user_signed_in?はユーザーがログインしているかどうかを確かめます。

例えば「ログインしている時としていない時で表示内容を変えたい」といった時に使える機能です。

今回はviewにこんな感じで書きました。

show.rb
<% if user_signed_in? %>
     <li class="nav-item active"><%= link_to("新規投稿", new_post_path,{class:"nav-link"}) %></li>
     <li class="nav-item active"><%= link_to("ログアウト", destroy_user_session_path,{method: :delete,data:{confirm: "ログアウトしますか?"},class:"nav-link"}) %></li>
<% else %>
     <li class="nav-item active"><%= link_to("新規登録", new_user_registration_path,{class:"nav-link"}) %></li>
     <li class="nav-item active"><%= link_to("ログイン", new_user_session_path,{class:"nav-link"}) %></li>
<% end %>

user_signed_in?していたら新規投稿・ログアウトを表示、していなかったら新規登録とログインを表示、という単純なものです。

これらもdeviceを使った機能なので基本的に書くだけで簡単に実装出来てしまうのですが、もし使わない場合だとインスタンス変数@current_userを定義するところから始めなければいけません。今回はそこは割愛しますが、その辺の仕組みもまた理解を深めるためにも記事にできたらと思います。

ユーザーと投稿の紐付け

最初に躓いたのはここでした。ユーザーと投稿を用意できたはいいがこれをどうやって紐付けるかが問題です。

私はプロゲートに倣ってPostモデルにuser_idカラムを追加しました。記事を投稿するときにuser_idカラムにcurrent_user.idを入れて紐づけるといった感じです。

posts_controller.rbでscaffoldで自動生成されたpost_paramsのpermitにuser_idを追加します。

posts_controller.rb
def post_params
       params.require(:post).permit(:title, :content, :user_id)
end

これでuser_idに値が届くようになります。

続いてapp/views/posts/_form.html.erbではuser_idを送信できるように以下のように変更します。

app/views/posts/_form.html.erb
<div class="field">
    <%= form.label :記事内容 %>
    <%= form.text_area :content,value:@post.content %>
    <%= form.hidden_field :user_id, value: current_user.id %>
</div>

form.hidden_fieldを設置して、こっそりuser_idテーブルにvalueに設定したcurrent_user.idを送ります。これで投稿とユーザーの紐付けはおkです。

redisの導入

ブログの形になったので最後にランキングを作ります。

正直ここが1番躓きました。というのも、redisの基本コマンドとかはなんとなく分かっていたのですが具体的にどういった時に使うのかわからなくてそもそもイメージがちゃんと出来てなかったからです。

今回使うredisはブログサイトでよくあるランキングを表示するために使います。

他にはセッション管理などに使っているのが調べてたら多く見られました。

まずRailsとredisの接続からしないといけないのですが、ここで2日くらい躓きました。色んなサイトを見まくって試したけど全然繋がらない状態が続き地獄でした。

結論から申し上げると、基本的にredisはlocalhost:6379に繋ぐのが普通なのですが、開発段階だとRails自体をlocalhostに繋いでるので混同しちゃって上手く繋がらなくなってしまっていたということでした。

なのでredisの設定をlocalhost→redisといった感じに名前を変更して行えばすんなり上手くいった感じです。

出来てしまえば簡単なことだったと思うのですが、やはり初心者には結構辛いところでした。コード打っててエラーならまだしも繋がらなくてエラーは精神的にかなりきます。

以下は変更点と参考サイトです。

https://qiita.com/sibakenY/items/0fff6398b8f832fb40a6】

https://teratail.com/questions/115631】

https://stackoverflow.com/questions/34729752/sidekiq-error-connecting-to-redis-on-127-0-0-16379-errnoeconnrefused-on-doc】

redisでランキング機能の実装

無事redisは導入出来ましたが、「導入出来てしまえばこっちのもん!」というわけではありません。

ランキングを表示しないといけないのでこれまたredisの基礎とcontroller、viewを見直さないといけません。

これも「Rails redis ランキング」と調べたら結構参考サイトは出てくるのですが、仕組みの理解が乏しいので基礎の見直しが必要でした。

redisの特徴としては以下のような感じです。

  • インメモリアルデータベース(すごく早い!)…ランキングなどに向いてる
  • 永続化(定期的にディスクに書き出す)
  • データ構造サーバー

そんなredisをRailsで使うには、methodを利用する必要があります。

今回はredisのソート済みセットを使ってランキングを実装していきました。

この辺は以下のサイトが非常に参考になったので貼っておきます。

https://qiita.com/yokozawa/items/aae59b53897ca12f7064】

https://qiita.com/sibakenY/items/0fff6398b8f832fb40a6】

https://blog.seishin55.com/entry/2016/05/02/214513】

また、pv数の表示はviewに直接以下のように書けば表示されます。

index.rb
<ul>
   <% @ranking_posts.each do |ranking_post| %>
       <li>
         <%= link_to(ranking_post.title,"/posts/#{ranking_post.id}") %>
         (<%= REDIS.zscore("posts/daily/#{Date.today.to_s}", ranking_post.id).to_i %>PV)
       </li>
   <% end %>
</ul>

アクションに設置する方法がないか考えたのですが、これしか方法がわからなかったです。ちょっと見苦しいですがとりあえずこれでPV数が表示されます。

まとめ

以上今回作ったブログサイトの大雑把なまとめでした。
初心者のメモ程度のものなので間違ってたり足りないところとかたくさんあると思いますが、初心者の方とかの参考になれば嬉しいです。
また、アドバイスなどあればコメントなどしてくれたら嬉しいです。

お付き合い頂きありがとうございました。

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

Ruby、PHP、Java、JSでの書き方の違い

はじめに

「Rubyでプログラミングの基礎を学んだから、次はjsやってみようかな!」

「書き方ごちゃごちゃになって、何が何だかよくわかんねー!!」

こんな人もいるのでは?

僕自身も以前、詰め込みすぎでパンパンになりましたので、書き方の違いをわかりやすくまとめてみました。
一つ一つ覚える必要はありませんので、この記事を、ストックしていただければと思います。
オブジェクト指向の理解も多少深まると思いますので、最後までご覧ください!

文字出力

基本となる文字の出力。言語によって処理のされ方が違うので難しいところですが、基本なのでしっかり押さえましょう。Javaはやっぱり記述が長いですねw

  #Ruby
  puts "こんにちは"
  #PHP
  echo "こんにちは";
  //Java
  System.out.println("こんにちは");
  //JavaScript
  console.log("こんにちは");

変数代入

プログラミングの超基本、変数代入です。

  #Ruby
  name = "Tom"
  #PHP
  $name = "Tom";
  //Java(String、intなどの型を指定する)
  String name = "Tom";
  //JavaScript
  let name = "Tom";

変数展開、文字列+変数

これもよく使いますね。全部似てますが、違いがあります。※書き方はあくまで一例です

  #Ruby
  "私の名前は#{name}です。"
  #PHP
  "私の名前は{$name}です。"
  //Java
  "私の名前は"+ name + "です。"
  //JavaScript(''ではなく``(バッククオーテーション)で囲む)
  `私の名前は${name}です。`

if文

処理の代表と言えばif文でしょう。elsifのところの違いに注意です。

  #Ruby
  if age >= 20
    #処理
  elsif age >= 10
    #処理
  else
    #処理
  end
  #PHP
  if (age>= 20){
    #処理
  }elseif (age >= 10){
    #処理
  }else{
    #処理
  }
  //Java & JavaScript
  if (age>= 20){
    //処理
  }else if (age >= 10){
    //処理
  }else{
    //処理
  }

配列と取り出し方

配列の取り出しもよく使いますね、1つづつ取り出す方法も言語によっていろんなのがあります。

  #Ruby
  names = ["Tom","Kenta","John"]
  names[0]
  #PHP
  $names = array("Tom","Kenta","John");
  $names[0];
  //Java
  String names[] = {"Tom","Kenta","John"};
  names[0];
  //JavaScript
  const names = ["Tom","Kenta","John"];
  names[0];

ハッシュ、連想配列、オブジェクトと取り出し方

言語によって呼び方が違うので、検索する際には注意してください。

  #Ruby(ハッシュ)
  user = {name: "Tom",age: 20}
  user[:name]
  #PHP(連想配列)
  $user = array("name" => "Tom","age" => 20)
  $user["name"]
  //JavaScript(オブジェクト)
  const user = {name: "Tom",age: 20};
  user.name

通常のメソッド、関数

ここからが重要なところです。今回は2つの値を合計するaddメソッド(関数)を作りました。
戻り値も重要なので、しっかり押さえましょう。

  #Ruby
  def add(a, b)
    return a + b
  end
  sum = add(5, 2)
  #PHP
  function add($a, $b){
    return $a + $b;
  }
  $sum = add(5, 2);
  //Java
  public static int add(int a, int b){
  //staticの後のintは戻り値の型を指定、戻り値がないメソッドの場合はvoidを使用
    return a + b; 
  }
  int sum = add(5, 2);
  //JavaScript
  const add = function(a, b){
    return a + b;
  };
  let sum = add(5,2);

  //または
  const add = (a, b) => {
    return a + b;
  };
  let sum = add(5,2);

  //または
  function add (a, b){
    return a + b;
  }
  let sum = add(5,2);

jsはバージョンによって推奨されている記述が違います。

クラスとインスタンス作成

さあ、待ちに待ったオブジェクト指向の始まりです!
Menuクラスを作って、そのクラスをもとに、インスタンスを作成し変数menu1に代入しています。

  #Ruby
  class Menu
    ##処理
  end
  menu1 = Menu.new
  #PHP
  class Menu{
    ##処理
  }
  $menu1 = new Menu();
  //Java
  class Menu{
    //処理
  }
  Menu menu1 = new Menu();
  //JavaScript
  class Menu{
    //処理
  }
  const menu1 = new Menu();

インスタンス変数、インスタンスフィールド、プロパティの定義

メニューには名前や値段などの"情報"が含まれます。それをクラス内で事前に宣言しておきます。

  #Ruby
  attr_accessor :name
  attr_accessor :price
  #PHP
  private $name;
  private $price;
  //Java
  private String name;
  private int price;
  //JavaScript
  //事前定義は不要?

初期メソッドでのインスタンス変数、プロパティへの代入

さあ、さらに訳わからないところにやってきました!
初期メソッドとはnewされた時(一番最初)に呼ばれるメソッドになります。
今回はnewの引数に指定された値が初期メソッド内で、インスタンスの情報として変数に入ります。

  #Ruby
  def initialize(name, price)
    self.name = name
    self.price = price
  end
  menu1 = Menu.new("ハンバーガー",300)
  #PHP
  public function __construct($name,$price){
    this->name = $name;
    this->price = $price;
  }
  $menu1 = new Menu("ハンバーガー",300);
  //Java
  //クラス内でクラスと同名のメソッドを定義する
  Menu(String name, int price){
    this.name = name;
    this.price = price;
  }
  Menu menu1 = new Menu("ハンバーガー",300)
  //JavaScript
  constructor (name, price){
    this.name = name;
    this.price = price;
  }
  const menu1 = new Menu("ハンバーガー",300)

インスタンスメソッドと呼び出し

さて最後になります。インスタンスは"情報"の他に"処理"を持っています。
今回はその処理の定義方法と呼び出し方です。
変数menu1にはMenuクラスから作られたインスタンスが代入されているので、
出力は"こちらのハンバーガーは300円です"となります。

  #Ruby
  def show
    puts "こちらの#{self.name}#{self.price}円です"
  end
  menu1.show
  #PHP
  public function show(){
    echo "こちらの{$this->name}{$this->price}円です";
  }
  $menu1->show();
  //Java
  public void show(){
    System.out.println("こちらの"+this.name+"は"+this.price+"円です");
  }
  menu1.show();
  //JavaScript
  show(){
    console.log(`こちらの${this.name}${this.price}円です`);
  }
  menu1.show();

終わりに

最後までご覧いただきありがとうございました。
抜けや間違いがあるかもしれませんが大目に見てください!!

この世の中では浅く広く学ぶのはあまり良くないとされていますが、知識を深めるために別言語に挑戦してみるのもいいと私は思います。

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

初学者によるプログラミングMemo #22 素因数分解

はじめに

今回は素因数分解のお話です
最近問題を解くことが多くなって、アルゴリズム関係に面白みを見出しています
Rails関係なくても書いている下記はすでに決まり文句ともなりました
なお、本記述はMacにおいて、Railsでの開発を前提としています
また、まだまだひよっこですので、不備等ございましたらご指摘いただけると幸いです→よく間違えますが、生暖かく見守ってくださると幸いです

目次

  • 素因数分解とは
  • 問題を解いてみる

素因数分解とは

素因数分解とは、ある正の整数を素数の積で表すことです
なんのこっちゃですかね?
具体的に見てみましょう
例えば「30」を素因数分解してみましょう

30 = 10 * 3
10 = 5 * 2
# ↑を合わせるとこう(↓)なる
30 = 2 * 3 * 5

2,3,5はいづれも素数ですね
このように、元の数を素数を使った掛け算で表すことを、素因数分解と言います

問題を解いてみる

さて、本題です
素因数分解を用いた問題を解いてみましょう

問題
"27156"の約数のうち、"100"以下の約数を全て答えよ
さあ、困った
なんせ約数の話は一切していないですからね
まあでも約数は小学生の範囲なんで割愛します、流石にわかるでしょうから

さて、お察しかと思いますが、素因数分解を利用することで、約数を求めることができます
順番にやっていきましょう
なお今回は、前々回の素数の話を使いますので、先にご覧になることを推奨します
メソッドはここに載ってます

#挙動確かめるために"20"で行います
require 'prime'

20.prime_division.each do |prime|
  p prime
end

# => [2, 2]
#    [5, 1]

prime_divisionメソッドを使うと、素因数分解を自動で行ってくれます便利ですねー、いや、本当にありがたい
で、返り値はご覧の通り、"[素数,その数]"となります
ではこの性質を利用してみましょう

require 'prime'

primes = []

20.prime_division.each do |prime|
  prime[1].times {primes << prime[0]}
end

p primes
# => [2, 2, 5]

素因数分解ができてますね
解説します
先ほどの返り値を元に、"primes"に"prime[0]"(素数自体)を代入しています
しかし、それだけでは1回しか代入されないので、その前に"times"メソッドを使って、"prime[1]"(素数の数)の回数分代入処理を行ってやります
これで、無事素因数分解ができました
あとは約数を作らないといけませんね

約数を作る

さて、問題ももう後半に差し掛かりました
あとはこれを組み合わせてあげれば約数が出ますね
そうです組み合わせです
書いていきましょう

require 'prime'
primes = []
divisors = []

20.prime_division.each do |prime|
  prime[1].times {primes << prime[0]}
end
1.upto(primes.size) do |i|
  primes.combination(i) do |prime|
    divisors << prime.inject{|a,b| a *= b}
  end
end

p divisors

# => [2, 2, 5, 4, 10, 10, 20]

先に挙動の解説をします
"combination"メソッドを使って、組み合わせを作ります
組み合わせる数は"primes"に入っている要素分行うので、その前に、"upto"メソッドを使って、数を回します
"prime.inject{|a,b| a *= b}"は、組み合わせで持ってきた各項を掛け算してもらうための処理です

うまいこといきまし…ん?
なんかおかしいですね、2点ありますが何かわかりますか?

まず、"2"と"10"が2回出てきてしまっています

そして…

"1"がないですね

致命的です、これではいけません

1がない

修正を加えます

require 'prime'
primes = []
divisors = [1] # <= ここ

20.prime_division.each do |prime|
  prime[1].times {primes << prime[0]}
end
1.upto(primes.size) do |i|
  primes.combination(i) do |prime|
    divisors << prime.inject{|a,b| a *= b}
  end
end

divisors.uniq! # <= ここ

p divisors

# => [1, 2, 5, 4, 10, 20]

これでいけましたね
"divisors = [1]"はなんか無理やり感があってあまり好きではないですが、仕方ありません(←成長を止める要因)
そして"divisors.uniq!"で、重複を削除しています

ここまでくればもう完成です

require 'prime'
primes = []
divisors = [1]
ans = []

27156.prime_division.each do |prime|
  prime[1].times {primes << prime[0]}
end
1.upto(primes.size) do |i|
  primes.combination(i) do |prime|
    divisors << prime.inject{|a,b| a *= b}
  end
end

divisors.uniq!
for i in divisors
  if i <= 100
    ans << i
  end
end

p ans

# => [1, 2, 3, 31, 73, 4, 6, 62, 93, 12]

できました
が、数字の並びが気持ち悪いので、"sort!"しましょう

今回の完成形

ついでにおなじみの対話形式に変えちゃいましょう

require 'prime'

def divisor(x,y)
  primes = []
  divisors = [1]
  ans = []
  x.prime_division.each do |prime|
    prime[1].times {primes << prime[0]}
  end
  1.upto(primes.size) do |i|
    primes.combination(i) do |prime|
      divisors << prime.inject{|a,b| a *= b}
    end
  end
  divisors = divisors.uniq.sort
  for i in divisors
    if i <= y
      ans << i
    end
  end
  return ans
end

p "ある数の約数のうち、決まった数値以下の約数を求めたいん?そんなんうちに任しとき"
p "ほんならまず約数を求めたい数を教えてやー"
x = gets.to_i
p "ありがとう、次はなんぼ以下の約数求めなあかんか教えてや"
y = gets.to_i
if x != 0 && y != 0
  if x <= y
    p "なんでやねん!そんなん約数全部に決まってるやん"
    p divisor(x,y)
  else
    p "うちの労働力は高うつくで(笑)"
    p divisor(x,y)
  end
else
  p "もぅ、いけずせんと、ちゃんと入力して!!"
end

以上、終わり!!

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

正規表現でTwitterURLからユーザー名とstatusを取得する【Ruby】

TwitterのURLについて

これがツイートのURLの例
https://twitter.com/yurinolog/status/1209290280328814593?s=20

httpsから始まり3つ目のyurinologがユーザー名、5つ目の数字の羅列がstatusと呼ばれる各ツイートに割り振られる番号。(語尾のs=20の正体はいまだに分からない。知ってる方いらしたら教えて頂きたいです)

書いたコードと結果

url = "https://twitter.com/yurinolog/status/1209290280328814593?s=20"

/(https|http):\/\/(twitter.com)\/([A-Za-z0-9_]*)\/(status|statues)\/(\d+)/.match(url)

puts $1 #=> http or https
puts $2 #=> twitter.com
puts $3 #=> user
puts $4 #=> status or statuses
puts $5 #=> status

使用した正規表現

正規表現は数度使ったことがあるが、抽象的というかコードから結果がイメージしにくく知識はほぼ皆無なので、一応復習しておくことにする。

キャプチャ()と$について

キャプチャ(後方参照)と言うのは正規表現でマッチした部分の1部を取り出すときに使う。

取り出す予定の部分を()で囲うことで$1$2といった風に取り出すことが出来る。

/(https|http):\/\/(twitter.com)/.match(url)
puts $2 #=> twitter.com

いくつかの候補から選択する

今回の場合はhttpsかhttpか、statusかstatusesとなる。

/(https|http)/.match(url)

候補となる値を|で区切る。

英数字とアンダーバーについて

今回は[A-Za-z0-9_]と書いた。
他にも
[A-Z]
[a-z]
[0-9]
と分けて表現できる。

あとstatusの数字の表現には/d+を使ってみた。

*+は同じ文字や単語を繰り返しマッチさせたいと気に使う。

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

rails new からrails serverまでの流れ

この記事はrails newしたい時に色々忘れていたりするので自分用のメモです
都度更新していくかもしれません。

rails newまで

gemはvendor/bundleで管理したい

  • フォルダ作成からAtomへ移動まで
~
❯ cd MyApp

~/MyApp
❯ mkdir portfolio

~/MyApp
❯ cd portfolio

~/MyApp/portfolio
❯ bundle init
Writing new Gemfile to /Users/kn428/MyApp/portfolio/Gemfile

~/MyApp/portfolio
❯ atom .
  • rails5.2の環境にしたかったので、Gemfileのgem "rails"のコメントアウトを外し、gem 'rails', '~> 5.2.1'とする
  • bundler経由でrailsをインストールする
~/MyApp/portfolio
❯ bundle install --path vendor/bundle --jobs=4
  • bundle exec経由でrails newにopを付けて実行する
    .は現在のディレクトリの意味
    opはrails new -hで確認またはググる
~/MyApp/portfolio 59s
❯ bundle exec rails new . -B -d mysql --skip-test
       exist
      create  README.md
      create  Rakefile
      create  .ruby-version
      create  config.ru
      create  .gitignore
    conflict  Gemfile
Overwrite /Users/kn428/MyApp/portfolio/Gemfile? (enter "h" for help) [Ynaqdhm] Y

Gemfileを上書きしていいか聞かれたらYで続行

参考 :
新規Railsプロジェクトの作成手順まとめ
rails new 手順書

Gemfileに必要なGemを追加

  • gemfileに下記をコピペする(※私の場合)
    汎用性のためSLIM等は外しておく
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.4'

gem 'rails', '~> 5.2.1'
gem 'bootsnap', require: false
gem 'mysql2', '~> 0.5.2'
gem 'puma', '~> 3.7'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'jquery-rails', '~> 4.3'
gem 'turbolinks',   '~> 5.0'
gem 'coffee-rails', '~> 4.2'
gem 'jbuilder', '~> 2.5'

group :development, :test do
  gem 'sqlite3', '~> 1.3.6'
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
  gem 'spring-commands-rspec'
  gem 'pry-rails'
  gem 'pry-doc'
  gem 'pry-byebug'
  gem 'rails-erd'
  gem 'annotate'
end

group :development do
  gem 'web-console', '>= 3.3.0'
  gem 'listen', '>= 3.0.5', '< 3.2'
  gem 'rubocop-airbnb'
  gem 'bullet'
end

group :test do
  gem 'capybara'
  gem 'webdrivers'
end

gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

これで$ bundle updateしたところ下記のエラーが発生

An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'` succeeds before bundling.

様々な対処法があるらしいが私は下記で対処できた
1. $ brew info opensslを実行し
export LDFLAGS="-L/usr/local/opt/openssl@1.1/lib"
export CPPFLAGS="-I/usr/local/opt/openssl@1.1/include"
この箇所の""内の部分をコピーしてメモ帳などで貼っておく(環境によるのでちゃんとコマンドを実行して確認すること)
2. それを下記のコマンドに繋げる
--with-cppflags
--with-ldflags
3. 最後にbundle configコマンドに繋げる
$ bundle config --local build.mysql2 "--with-cppflags=-I/usr/local/opt/openssl@1.1/include"
$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib"
すると.bundle/config内に追加されている(--with-cppflagsのほうが上書きされている気がするが気にしないでおく)

.bundle/config
---
BUNDLE_PATH: "vendor/bundle"
BUNDLE_JOBS: "4"
BUNDLE_BUILD__MYSQL2: "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib"

これで$ bundle updateしたら無事全てインストールできた

参考 :
mysql2 gemインストール時のトラブルシュート
Bundlerでビルドオプションを指定する

installしたGemの設定

Rspec関連を設定する

$ bundle exec rails generate rspec:install
.rspecのフォルダの中に--format documentationを追加する
config/application.rb内を下記の状態に変更する

.config/application.rb
module Portfolio
  class Application < Rails::Application
  # ...省略...
    config.time_zone = 'Tokyo'
    config.generators do |g|
      g.test_framework :rspec,
                       view_specs: false,
                       helper_specs: false,
                       controller_specs: false,
                       routing_specs: false,
                       request_specs: false
    end
    config.generators.system_tests   = false
    config.generators.stylesheets    = false
    config.generators.javascripts    = false
    config.generators.helper         = false
  end
end

spec/rails_helper.rbに以下を追記

RSpec.configure do |config|
  # ...省略...
  config.include FactoryBot::Syntax::Methods # 追加
end

テスト用DBをマイグレーションしておく
$ bundle exec rails db:migrate RAILS_ENV=test

  • Capybaraの初期設定
    $ mkdir spec/supports
    $ touch spec/supports/capybara.rb
    spec/supports/capybara.rb内に下記を実装
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless
  end
end

spec/spec_helper.rbに下記を追加
require 'supports/capybara'

  • SystemとRequest Specの追加
    $ mkdir spec/system
    $ mkdir spec/requests

  • Rspecの高速化
    $ bundle exec spring binstub --all
    $ bin/rspec spec/のコマンドで動けばok。direnvの設定は次回の機会にまわす

参考 :
RSpecを導入してテストを書いてみる
spring と direnv を使って Rails と rspec を高速起動。快適開発はじめる

rails server

$ bundle exec rails db:create
$ bundle exec rails db:migrate
$ bundle exec rails s
無事(http://localhost:3000/) に「Yay! You’re on Rails!」が表示されれば完了

git init ~ push

使いまわせたら嬉しいのでここまでをgithub等にpushしておく
まず.gitignoreの中に/vendor/bundleを入れておく
$ echo '/vendor/bundle' >> .gitignore

$ git init
$ git add .
$ git commit -m "first commit"
$ git remote add origin https://github.com/ユーザー名/リポジトリ名.git
$ git push -u origin master

以上です。何かアドバイス等ありましたらコメントいただけると嬉しいです。

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

中間テーブルとは??

 プログラミング初学者が学習する中間テーブルについて

  • Railsなどのフレームワークを学習していて『中間テーブル』という言葉がよくわからなかったので備忘録も兼ねて解説します! (初投稿なので温かく見守ってください泣)

  • 中間テーブルってなんなの??

  • 具体的な使い方

中間テーブルってなんなの??

中間テーブルとは・・・データベースで、テーブルとテーブルの多対多の関係を表すテーブルのこと。

これだけ言われてもパッとしないかもしれません。イメージしてみましょう!

データベースにuserテーブルとgroupテーブルというのが存在すると仮定します。

userテーブル

id name
1 山田
2 佐藤
3 鈴木
4 田中

groupテーブル

id group-name
1 Aグループ
2 Bグループ
3 Cグループ
4 Dグループ

以上二つのテーブルから誰がどのグループに属しているかを表現してみると、、

id name group
1 山田 Bグループ
2 佐藤 Cグループ
3 鈴木 Aグループ
4 田中 Cグループ

このようにuserテーブルにgroupカラムを追加することで解決できます。

しかし!

userがいくつものgroupに所属できるようにしたい!と考えました
*例えば通話アプリの『LINE』でも複数の『グループ』に所属できますよね!

すると、、

userテーブル

id name group1 group2 group3 group4
1 山田 Bグループ Cグループ Aグループ
2 佐藤 Cグループ Aグループ Dグループ Bグループ
3 鈴木 Aグループ
4 田中 Cグループ Bグループ
これじゃ、空のカラムが大量発生するじゃないか!!!(親父ギャグ)
  • カラムどんだけ追加しないとダメなんだ・・・
  • DB設計時にどれだけカラムを用意しなければならないんだろう・・・予測できない・・
  • 空のカラムはエラーの元じゃないか・・・

ここで登場するのが 中間テーブル です

中間テーブルの具体的な使い方

userテーブル

id name
1 山田
2 佐藤
3 鈴木
4 田中

groupテーブル

id group-name
1 Aグループ
2 Bグループ
3 Cグループ
4 Dグループ

新たに以下のテーブルを追加!

group_userテーブル

id user group
1 山田 Bグループ
2 山田 Cグループ
3 山田 Aグループ
4 佐藤 Cグループ
5 佐藤 Aグループ
6 佐藤 Dグループ
7 佐藤 Bグループ
8 鈴木 Aグループ
  1. このように中間テーブルを追加することで空のカラムもなくなり、その都度追加する必要があったカラムも追加せずに済むことができます。さらに誰がどのグループに所属しているかが見やすくなりました。

  2. データベースの設計をしていく際には空のカラムが発生しないか、チームとして開発を進めていく中で見やすいDBができているかを考える必要があると思うので覚えておきましょう!!!

初投稿となりましたが、何かわかりにくい点や誤って理解してしまっている点などあればご教授願います。では!

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

Rails ユーザーごとに複数の一覧ページのフラグメントキャッシュを作成する

始めに

プログラミング初学者のQiita初投稿です。
至らない点も多くあると思いますが、頑張って最近やったことを書いてみようと思います。

なにをするか

まず、ユーザーが投稿した記事を一覧表示したページが複数ある
今回は全ての記事一覧と、ユーザーがお気に入り登録した記事一覧を作成

この2つのページのフラグメントキャッシュをユーザーごとに分けて作成する
キャッシュを通して全てのユーザーに同じページが表示されてしまうのを防ぐため、ユーザーごとに分けます。

環境

  • Ruby 2.6.2
  • Rails 5.2.2
  • devise 4.7
  • Redis 4.1

ModelとControllerの設定

Modelは
User, Post, Likeの3つ
ユーザーと、ユーザーが投稿する記事と、その記事をお気に入り登録するためのモデルです。
ユーザーはdeviseを使って作成しました。

app/models/user.rb
class User < ApplicationRecord
  has_many :posts
  has_many :likes
  has_many :like_posts, through: :likes, source: 'post'
end
app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :likes
  has_many :like_users, through: :likes, source: 'user'
end
app/models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :post
end

ControllerはPostsControllerに2つのページのアクションを用意します

app/controllers/posts_controllers.rb
class PostsController < ApplicationController

  def index
    @posts = Post.all
  end

  def like_index
    @posts = current_user.like_posts
  end

記事を投稿してお気に入り登録するまでの流れは、今回の本題とは離れるので割愛します。

記事一覧ページの作成

一覧表示部分は部分テンプレートで共通化します

app/views/posts/index.html.erb
<h1>記事一覧</h1>

<%= render partial: 'posts_index', locals: { posts: @posts } %>
app/views/posts/like_index.html.erb
<h1>お気に入り記事一覧</h1>

<%= render partial: 'posts_index', locals: { posts: @posts } %>
app/views/posts/_posts_index.html.erb
<% posts.each do |post| %>
  <%= post.title %>
  <%= post.user.name %>

  ↓お気に入り登録ボタン↓
  <% unless post.like_users.include?(current_user) %>
    <%= link_to likes_path(user_id: current_user.id, post_id: post.id), method: :post do %>
      <p>お気に入り登録</p>
    <% end %>
  <% else %>
    <%= link_to like_path(id: post.id), method: :delete do %>
      <p>お気に入り解除</p>
    <% end %>
  <% end %>
 ↑お気に入り登録ボタン↑

<% end %>

一覧表示部分には、ワンクリックで記事をお気に入り登録・解除できるリンクを記述しました。

キャッシュの作成

ここからキャッシュを作っていきます。
キャッシュストアにはRedisを使用しています。
Railsはデフォルトでキャッシュがオフになっているので、以下のコマンドでキャッシュをオンにします。

rails dev:cache

設定ファイルで有効期限を設定できます。

config/environments/development.rb
config.cache_store = :redis_store, { expires_in: 1.hour }

フラグメントキャッシュを作成するのは、以下のようにして簡単にできます。

app/views/posts/_posts_index.html.erb
<% cache 'post_index' do %>  追加
<% posts.each do |post| %>
  <%= post.title %>
  <%= post.user.name %>

  ↓お気に入り登録ボタン↓
  <% unless post.like_users.include?(current_user) %>
    <%= link_to likes_path(user_id: current_user.id, post_id: post.id), method: :post do %>
      <p>お気に入り登録</p>
    <% end %>
  <% else %>
    <%= link_to like_path(id: post.id), method: :delete do %>
      <p>お気に入り解除</p>
    <% end %>
  <% end %>
 ↑お気に入り登録ボタン↑

<% end %>
<% end %>  追加

これで、'post_index'というキーで指定した範囲をキャッシュできます。

しかし、このままでは2つのページのキャッシュキーが同じなため、ページ内容が同じになってしまいます。
ここでは、アクションで別々のキャッシュキーを作成し、変数に入れることで対処します。

ページごとにキャッシュを分ける

app/controllers/posts_controllers.rb
class PostsController < ApplicationController

  def index
    @cache_key = 'index'

    @posts = Post.all
  end

  def like_index
    @cache_key = 'like_index'

    @posts = current_user.like_posts
  end
app/views/posts/_posts_index.html.erb
<% cache @cache_key do %>  変更
<% posts.each do |post| %>
  <%= post.title %>
  <%= post.user.name %>

これで、ページごとにキャッシュを分けることができます。

続いてユーザーごとにキャッシュを分けられるようにしましょう。
今のままだと全ユーザーに同一のページが見えてしまいます。
お気に入りに登録した記事を見ようとしたら他人がお気に入りにした記事一覧が表示された、なんてことになります。
もしくは自分のお気に入り記事一覧ページがキャッシュされた場合、他の全ユーザーにそれが行き渡ります。

ユーザーごとにキャッシュを分ける

app/controllers/posts_controllers.rb
class PostsController < ApplicationController

  def index
    @cache_key = ['index', current_user.id]

    @posts = Post.all
  end

  def like_index
    @cache_key = ['like_index', current_user.id]

    @posts = current_user.like_posts
  end

ユーザーのIDをキャッシュのキーに含ませることで、ユーザーごとに違うキャッシュが作成されるようになります。

ここまでで、ユーザーごとに複数の一覧ページのキャッシュを作成できましたが、まだ深刻な問題があります。
新しく記事が投稿されたり、ユーザーや記事の名前が変更された場合、キャッシュがあるせいでそれがビューに反映されません。
データが変わった時にはキャッシュのキーも変更することでこれを回避できるので、キーにはデータの最新情報を含ませるようにします。

最新のデータを反映できるようにする

app/models/application_record.rb
  scope :latest, -> { order(updated_at: :desc).first }
app/controllers/posts_controllers.rb
class PostsController < ApplicationController

  def index
    @cache_key = ['index', current_user.id, User.latest.update_at, Post.latest.update_at, Like.latest.update_at]

    @posts = Post.all
  end

  def like_index
    @cache_key = ['like_index', current_user.id, User.latest.update_at, Post.latest.update_at, Like.latest.update_at]

    @posts = current_user.like_posts
  end

これで、データの変更時には新しくキャッシュが作成されるようになりました。

削除が反映されない問題

ここまでで基本的なフラグメントキャッシュを作成することができました。
しかしまだ問題はあります。
データの変更時にキャッシュを新しく作成することはできましたが、
データを削除した時はキャッシュは変わりません。(最新のデータであれば変わります)

例えば、
記事1を投稿する キャッシュ1ができる
記事2を投稿する キャッシュ2ができる
ここで記事1を削除する 最新の更新時間は変わらないためキャッシュはできない
記事一覧ページのロードには最新のキャッシュ2が使われる
キャッシュ2ができた時は記事1は存在したため、記事1は表示される

というように、
削除された記事1が、削除後も表示されてしまいます。
データ変更時の時間を、削除された時間で上書きする必要がありそうです。
そこでデータが削除された時間を取得する方法を探してみたら、論理削除というものを発見しました。
論理削除はデータが削除された時、レコードを消去せずに削除時間を入力して削除されたものとみなすもののようです。
この削除時間が入力された時、更新時間(updated_at)も変わるため、今回の問題にはこれが使えそうです。
もっといい方法があるかもしれませんが、今回は論理削除を活用してみようと思います。

論理削除

Userは削除されないものとして、
PostとLikeモデルに論理削除を適用します。
まずパラノイアというGemをインストールします。

gem 'paranoia'
$ bundle install

論理削除を使うモデルに、deleted_atカラムを追加します。

$ rails g migration AddDeletedAtToPosts deleted_at:datetime
$ rails g migration AddDeletedAtToLikes deleted_at:datetime
class AddDeleteAtToPosts < ActiveRecord::Migration[5.2]
  def change
    add_column :posts, :deleted_at, :datetime
  end
end
class AddDeleteAtToLikes < ActiveRecord::Migration[5.2]
  def change
    add_column :likes, :deleted_at, :datetime
  end
end
$ rails db:migrate

モデルファイルにacts_as_paranoidを記述します。

app/models/post.rb
class Post < ApplicationRecord
  acts_as_paranoid

  belongs_to :user
  has_many :likes
  has_many :like_users, through: :likes, source: 'user'
end
app/models/like.rb
class Like < ApplicationRecord
  acts_as_paranoid

  belongs_to :user
  belongs_to :post
end

これだけで論理削除が適用されるようになりました。
これで記事やお気に入りが削除された際、レコードは実際には消えず
削除時間が追加されるだけになりました。

データ検索の際に従来の、

Post.all

だと、削除されたデータは除外されますが

Post.with_deleted.all

とすると、削除されたデータも含めて検索できます。

改めて、

app/controllers/posts_controllers.rb
class PostsController < ApplicationController

  def index
    @cache_key = ['index', current_user.id, User.latest.update_at, Post.with_deleted.latest.update_at, Like.with_deleted.latest.update_at]

    @posts = Post.all
  end

  def like_index
    @cache_key = ['like_index', current_user.id, User.latest.update_at, Post.with_deleted.latest.update_at, Like.with_deleted.latest.update_at]

    @posts = current_user.like_posts
  end

とすることで、

記事1を投稿する キャッシュ1ができる
記事2を投稿する キャッシュ2ができる
記事1を削除する 最新の更新時間が変わったためキャッシュ3ができる
記事一覧ページのロードには最新のキャッシュ3が使われる
キャッシュ3ができた時は記事1はいないため、記事1は表示されない

というように、
削除された記事1を一覧表示から消すことができました。

最後にキャッシュキー作成コードを、DRYにして終わりです。

app/controllers/posts_controllers.rb
class PostsController < ApplicationController

  def index
    @cache_key = make_cache_key('index')

    @posts = Post.all
  end

  def like_index
    @cache_key = make_cache_key('like_index')

    @posts = current_user.like_posts
  end

  def make_cache_key(action)
    [action, current_user.id, User.latest.update_at, Post.with_deleted.latest.update_at, Like.with_deleted.latest.update_at]
  end

後になって気づいたこと

さて、ここまでやってきた最中で、
今までのことの多くが無駄だったことに気づきます。

app/views/posts/_posts_index.html.erb
<% cache @cache_key do %>  これを
<% posts.each do |post| %>
  <%= post.title %>
  <%= post.user.name %>
app/views/posts/_posts_index.html.erb
<% posts.each do |post| %>
<% cache post.updated_at do %>  こうする
  <%= post.title %>
  <%= post.user.name %>

これだけでよかった!
eachで回される1つ1つのオブジェクト自体をキャッシュすることで、
自然とユーザーやページごとに別の内容になります。
コントローラーにも何も書かなくていいです。
今思えばこういう風に書いてるサイトが多かったのになぜこうしなかったのか、、、

ただ、今回に関してはこれだけではダメです。
今回は一覧表示されている記事1つ1つに、お気に入りボタンがありました。
上記のコードは記事1つ1つの更新時間をキャッシュキーにしているだけなので、お気に入り登録の変更は感知してくれません。
記事作者の情報が変更されても感知しません。
それに、お気に入りボタンは人によって見え方が違うはずなので、
ユーザーを識別するためにユーザーIDをキャッシュキーに含める必要が出てきます。
つまり今までやったようなことが結局必要になります。

app/controllers/posts_controllers.rb
class PostsController < ApplicationController

  def index
    @cache_key = make_cache_key

    @posts = Post.all
  end

  def like_index
    @cache_key = make_cache_key

    @posts = current_user.like_posts
  end

  def make_cache_key
    [Like.with_deleted.latest.update_at]
  end
app/views/posts/_posts_index.html.erb
<% posts.each do |post| %>
<% cache [current_user.id, room.updated_at, room.user.updated_at, @cache_key] do %>  
  <%= post.title %>
  <%= post.user.name %>

大体こんな感じになるでしょうか。
結局最初にやったのと同じような感じにはなりました。
一覧表示全体を1つのキャッシュにするか、一覧内容1つ1つをキャッシュするか。
どちらの方が良いかは時と場合によるでしょうか?

最後に

今回、お気に入りボタンなどもキャッシュしてみましたが、
そもそもユーザーによって表示が変わる、いわゆる動的な部分はキャッシュしないのが普通なのかもしれません。
論理削除も積極的に使うべきではないと思うので、全体としてあまり有意義なことはできなかったかもです。
それでも今回得られたキャッシュについての知見は、どこかで必ず役に立つとは思います。

参考資料

Railsのフラグメントキャッシュについて調べてみた

Railsのキャッシュ機構

Railsでparanoiaを使って論理削除を実装する

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

開発環境の Ruby version を上げる

開発環境 (最近変なスペルになった macOS をここでは想定) に Ruby を入れるのは、よくある話だが、Ruby をバージョンアップする手順についてはもサッとできるようにまとめておく。

前提

  • macOS にいれるミドルウェアのパッケージマネジメントは Homebrew で行っている
  • Ruby は直接 Homebrew でいれるのではなく、rbenv を用いて管理している
  • Ruby バージョンアップするのは久しい

つまり macOS > Homebrew > rbenv > bundler > 各フォルダごとに Gem file のような構成である。

コマンド

rbenv にてインストール可能なバージョンは下記で確認できる。

$ rbenv install -l

2.6.0
2.6.1
2.6.2
2.6.3
2.6.4
2.6.5

もし、必要なバージョンが存在しない場合は rbenv 自体をバージョンアップしてあげる必要があるだろう。

# Homebrew 本体を更新
$ brew doctor
$ brew update

# ruby-build を更新
$ brew upgrade ruby-build

必要なバージョンがあるかどうか確認して、global と local の version を設定しておく。

# Ruby
$ rbenv install -l
$ rbenv install 2.6.X

$ rbenv global 2.6.X
$ cd /to/your/app/path
$ rbenv local 2.6.X

# Bundler は各バージョンの Ruby ごとにインストール。
$ rbenv versions
$ gem install bundler

ふぅ

・・・ Docker 化しておいて構成まるごとごと差し替えるのがナウくてハマりづらい方法だとは思う。

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

Rubyで始めるスクレイピング 〜入門編〜

スクレイピングとは

Webサイトから自分の知りたい情報を抽出すること。
ex) 文章、画像、動画など

TL; DR

Qiitaで「ruby」で検索して「いいね順」に並べた検索結果一覧をスクレイピングします。
コードを見ながら説明します。

1. まずは前準備。

scraping01.rb
# コードのsyntax highlight・補完などをしてくれるgem(無くてもいいけどめっちゃ便利)
# https://github.com/janlelis/irbtools
require 'irbtools/more'

# URLにアクセスした内容をファイルのように扱えるgem
require 'open-uri'

# スクレイピングのgem
require 'nokogiri'

# CSV出力するgem
require 'csv'

url = 'https://qiita.com/search?page=1&q=ruby&sort=like'

URLのパラメータの仕組みについて

https://qiita.com/search?page=1&q=ruby&sort=like
?以降がパラメーターです。
パラメータを複数つなげるには&で繋ぎます。

今回のURLですと、pageqsortというパラメータがあります。
page=の後に続く値がページ番号になります。

Qiitaの場合は、q=の後に続く値が検索ワードになっています。
試しに、以下のURLを打ってみてください。
https://qiita.com/search?q=アイウエオ

sort=に続く値が並び順になっているようです。
sort=は受け取る値によって並び順が変わるようです。

パラメータ 並び順
like いいね順
created 新着順
rel 関連順
stock ストック順

また、上記に該当しない値の場合は、関連順になるようです。
https://qiita.com/search?q=ruby&sort=テストトト

Let's Parse!!!

scraping02.rb
# openブロック内に入る前に変数定義をしておかないと、openブロック内でしか使えないスコープ変数になってしまう為、ここで定義する必要がある。
charset = nil
# 「open(url)」でそのサイトの情報を取得。
html = open(url) do |f|
  # 文字コードはそのサイトの文字コードに合わせる。
  charset = f.charset
  #「.read」でHTML情報を出力
  f.read
end

# html変数に入っている情報はただの文字列です。
html.class
#=> String
# つまり、HTML要素もただの文字情報として認識されている(文章とHTML要素の区別がなされていない)と分かります。


# なので、HTML要素とそうでない文字列に分解(構文解析「parse」)する必要があります。
# そこで登場するのが、Nokogiriです。
# こんな感じで書きます。
Nokogiri::HTML.parse(構文解析したい文章, URL, 文字コード)
# https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri%2FHTML.parse

doc = Nokogiri::HTML.parse(html, nil, charset)


doc.class
#=> Nokogiri::HTML::Document
# これでHTML要素とそうでない文字列を分けて扱えるデータになりました。

Let's Nokogiri!!!

scraping03.rb
# 取得したい情報のHTML要素を「.css」メソッドに渡して取得します。
# 渡すHTML要素は、HTMLにCSSを当てる時のCSSセレクタ(HTMLタグやクラス、ID)を指定する記述方法と同じです。
# 例えば、検索結果一覧から「記事タイトル」を取得するには
doc.css('h1.searchResult_itemTitle').text
#=> "Markdown記法 チートシートペアプログラミングして気がついた新人プログラマの成長を阻害する悪習プログラミングでよく使う英単語のまとめ【随時更新】非デザイナーエンジニアが一人でWebサービスを作るときに便利なツール32選【まとめ】これ知らないプログラマって損してんなって思う汎用的なツール 100超新人プログラマに知っておいてもらいたい人類がオブジェクト指向を手に入れるまでの軌跡もう保守されない画面遷移図は嫌なので、UI Flow図を簡単にマークダウンぽく書くエディタ作った翻訳: WebAPI 設計のベストプラクティス開設後3週間で収益10万円を得た個人開発サイトでやったことの全部を公開するエンジニアの情報収集法まとめ"

# もちろん結果は、セレクタでヒットするかずを全部持ってきます。
doc.css('h1.searchResult_itemTitle')
#=> 10

# 結果を見やすくしてみましょう。
# 「.search」は「.css」のエイリアスメソッド
# 「.inner_text」は「text」のエイリアスメソッド
titles = doc.search('.searchResult_itemTitle').map{ |node| node.inner_text }


# これを応用して、「タイトル」と「タグ」と「本文」を取ってきましょう。
results = []
doc.search('.searchResult_main').each_with_index do |node, i|
  tags = []
  title = node.css('.searchResult_itemTitle').inner_text
  node.css('.tagList_item').each{ |article_tag| tags << article_tag.inner_text }
  details = node.css('.searchResult_snippet').inner_text
  results << [ title, tags, details ]
  p tags
end

results.each_with_index do |res, i|
  puts
  puts "#{i+1}番目の検索結果"
  puts "Title: #{res[0]}"
  puts "Tags: #{res[1]}"
  puts "Details: #{res[2]}"
  puts '-----------------------------------------'
end

Let's Scraping!!!

scraping04.rb
# 検索ワードを配列で持ちます。
search_terms = ['ruby', 'php', 'python', 'perl']

results = {}

search_terms.each do |search_term|
  query_hash = []
  # 検索結果一覧ページが100ページまでしか無いので1から100にしています。
  (1..100).each do |i|
    # URLはベタがきではなく、「ページ番号」と「検索ワード」に式展開を使い、柔軟にしています。
    url = "https://qiita.com/search?page=#{i}&q=#{search_term}&sort=like"
    charset = nil
    html = open(url) do |f|
      charset = f.charset
      f.read
    end
    doc = Nokogiri::HTML.parse(html, nil, charset)
    # 「.searchResult」にタイトルやタグなどの親クラスで、各記事の情報がぶら下っているので、一度ここを取得し、ここから子クラスの情報を抽出します。
    # ここからはRubyというより、CSSセレクタの内容になります。欲しい情報に応じてHTMLタグやクラスを書き換えてください。検証ツールで確認できます。
    doc.search('.searchResult').each do |node|
      tags = []
      title = node.css('.searchResult_itemTitle').text
      node.css('.tagList_item').each{ |article_tag| tags << article_tag.text }
      details = node.css('.searchResult_snippet').text
      link = "https://qiita.com/" + node.css('.searchResult_itemTitle').css('a')[0][:href]
      # [0]が「良いね」の数。[1]は「コメント」の数
      stars = node.css('.list-unstyled.list-inline.searchResult_statusList li')[0].text.gsub(" ","")
      # コメント数が0個の場合は、コメントのHTML要素が出力されない為、[1]のところでNilエラーになります。それを回避する為にアンパサンドを使用します。
      comments = node.css('.list-unstyled.list-inline.searchResult_statusList li')[1]&.text&.gsub(" ","")
      author = node.css('.searchResult_header').css('a')&.text
      query_hash << { stars: stars, title: title, tags: tags, details: details, link: link, comments: comments, author: author }
    end
    # この一連の処理は時間がかかります。ここでputsすることで、処理がどれぐらい進んでいるかを確認しながら待つことができます。
    puts "#{search_term} #{i}"
  end
  results["#{search_term}"] = query_hash

end



# resultsというハッシュに入った結果を一元配列で管理し、結果をCSV出力する。
ruby_array, php_array, python_array, perl_array = [], [], [], []

results["ruby"].each do |a|
  ruby_array << [ a[:stars], a[:title], a[:tags], a[:details], a[:link], a[:comments], a[:author] ]
end
# CSV.openの引数に、書き出すファイル名を指定します。その際にLinuxコマンドのようにPathで指定すれば任意の場所に保存できます。
CSV.open('qiita_ruby.csv', 'w') do |csv|
  # ヘッダーの設定
  csv << ['stars', 'title', 'tags', 'details', 'link', 'comments', 'author']
  ruby_array.each do |r|
    csv << r
  end
end

results["php"].each do |a|
  php_array << [ a[:stars], a[:title], a[:tags], a[:details], a[:link], a[:comments], a[:author] ]
end
CSV.open('qiita_php.csv', 'w') do |csv|
  # ヘッダーの設定
  csv << ['stars', 'title', 'tags', 'details', 'link', 'comments', 'author']
  php_array.each do |r|
    csv << r
  end
end

results["python"].each do |a|
  python_array << [ a[:stars], a[:title], a[:tags], a[:details], a[:link], a[:comments], a[:author] ]
end
CSV.open('Desktop/qiita_python.csv', 'w') do |csv|
  # ヘッダーの設定
  csv << ['stars', 'title', 'tags', 'details', 'link', 'comments', 'author']
  python_array.each do |r|
    csv << r
  end
end

results["perl"].each do |a|
  perl_array << [ a[:stars], a[:title], a[:tags], a[:details], a[:link], a[:comments], a[:author] ]
end
CSV.open('Desktop/qiita_perl.csv', 'w') do |csv|
  # ヘッダーの設定
  csv << ['stars', 'title', 'tags', 'details', 'link', 'comments', 'author']
  perl_array.each do |r|
    csv << r
  end
end


まとめ

  1. 指定したURLの情報を取得
  2. 取得した情報を構文解析
  3. CSSセレクタの記法を使って欲しい情報を抽出
  4. CSVに吐き出す

こんな感じでスクレイピングできます!!!
コメントや指摘をいただけると幸いです!!!

P.S.

rubyのスクレイピングは人気なさそう。。。
やっぱりライブラリや記事・文献の多さから、Pythonが圧倒的に人気っぽそうですよねぇ。。。
https://trends.google.co.jp/trends/explore?q=ruby%20scraping,python%20scraping,php%20scraping,perl%20scraping,javascript%20scraping

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

Leetcode: Swap Nodes In Pairs

require 'minitest/autorun'
require 'pry'
class SwapNodesInPairs < Minitest::Test

  def test_run
    list = ListNode.new(1)
    list.next = ListNode.new(2)
    list.next.next = ListNode.new(3)
    list.next.next.next = ListNode.new(4)
    new_list = swap_pairs(list)
    assert_equal(2, new_list.val)
    assert_equal(1, new_list.next.val)
    assert_equal(4, new_list.next.next.val)
    assert_equal(3, new_list.next.next.next.val)


    list = ListNode.new(1)
    new_list = swap_pairs(list)
    assert_equal(1, new_list.val)
  end

  def swap_pairs(head)
    return if head.nil?
    return head if head.next.nil?
    head_val = head.val
    next_head_val = head.next.val
    head.val = next_head_val
    head.next.val = head_val
    swap_pairs(head.next.next)
    head

  end

end

class ListNode
  attr_accessor :val, :next

  def initialize(val)
    @val = val
    @next = nil
  end

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

Leetcode: Reverse String

require 'minitest/autorun'
require 'pry'
class ReverseString < Minitest::Test

  def test_run
    input = ["h", "e", "l", "l", "o"]
    output = ["o", "l", "l", "e", "h"]

    assert_equal(output,
                 reverse_string(input))
  end

  def reverse_string(s, index = 0)
    return s if index == s.size / 2

    first_char = s[index]
    last_char = s[s.size - 1 - index]
    s[index] = last_char
    s[s.size - 1 - index] = first_char
    index += 1
    reverse_string(s, index)
    s
  end

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

Rubyのメソッドを再実装する勉強法(Enumerable#chunkを再実装)

社内で定期的にやっているRubyのメソッドを再実装する勉強法の紹介です。
※ メソッドの再実装はGrow.rbを参考にしています。

Rubyのメソッドを再実装することでRubyの理解を深め基礎力をつけることを目的にしています。
複数メンバーでやることで十人十色な実装、書き方に触れることができるので面白いです。

今回はEnumerable#chunkを再実装します。
※問題はGitHubにまとめています。

Enumerable#chunk

要素を前から順にブロックで評価し、その結果によって要素をチャンクに分けた(グループ化した)要素を持つ Enumerator を返します。
ブロックの評価値が同じ値が続くものを一つのチャンクとして取り扱います。すなわち、ブロックの評価値が一つ前と異なる所でチャンクが区切られます。
返り値の Enumerator は各チャンクのブロック評価値と各チャンクの要素を持つ配列のペアを各要素とします。そのため、eachだと以下のようになります。

[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].chunk {|n|
  n.even?
}.each {|even, ary|
  p [even, ary]
}
#=> [false, [3, 1]]
#   [true, [4]]
#   [false, [1, 5, 9]]
#   [true, [2, 6]]
#   [false, [5, 3, 5]]

引用:module Enumerable (Ruby 2.7.0 リファレンスマニュアル)

? 問題

chunk.rb
module Enumerable
  def chunk
    puts 'reimprement here'
  end
end

ここにchunkを再実装して正しく動くか確認します。

? テストコード

chunk_test.rb
require 'minitest/autorun'
require_relative 'chunk'

class ChunkTest < MiniTest::Test
  # chunk {|elt| ... } -> Enumerator
  def test_chunk
    chunk_result = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].chunk(&:even?)
    expected_result = [[false, [3, 1]], [true, [4]], [false, [1, 5, 9]], [true, [2, 6]], [false, [5, 3, 5]]]

    assert_equal expected_result, chunk_result.map { |even, ary| [even, ary] }
    assert_equal Enumerator, chunk_result.class
  end
end

chunk_test.rbchunk.rb と同じディレクトリに置き以下を実行。
テストが通れば成功です?

ruby chunk_test.rb

? メンバー回答例

今回は以下の要件はスキップして実装していました。

ブロックの評価値が nil もしくは :separator であった場合、 その要素を捨てます。チャンクはこの前後で区切られます。
ブロックの評価値 :
alone であった場合はその要素は 単独のチャンクをなすものと解釈されます。

※ 正解では無いのであしからず

回答例1

module Enumerable
  def chunk &block
    bool = nil
    result = []
    temp_result = []

    # 1つめの真偽値を保存する
    # 真偽値が切り替わるまでループする

    each do |item|
      temp_bool = yield item
      if temp_bool != bool
        bool = temp_bool
        result << temp_result
        temp_result = [bool, [item]]
      else
        temp_result[1] << item
      end
    end

    result << temp_result
    result.shift
    result.each
  end
end

回答例2

module Enumerable
  def chunk
    results = []
    now_result = []

    each do |item|
      value = yield item
      if now_result[0].nil?
        # はじめて
        now_result[0] = value
        now_result[1] = [item]
      elsif now_result[0] == value
        # おなじ
        now_result[1] << item
      else
        # ちがう
        results << now_result
        now_result = []
        now_result[0] = value
        now_result[1] = [item]
      end
    end

    results << now_result

    results.each
  end
end

回答例3

module Enumerable
  def chunk(&block)
    result = []
    tmp = []
    block_result = nil
    each do |n|
      tmp_block_result = yield(n)
      if !tmp.empty? && block_result != tmp_block_result
        result << [block_result, tmp]
        tmp = []
        block_result = tmp_block_result
      end
      block_result = tmp_block_result
      tmp << n

    end
    result << [block_result, tmp]
    result.each
  end
end

最後に

Rubyのメソッド再実装は

  • ドキュメントを読む
  • 仕様を正しく理解する
  • 仕様通りに実装する

という基本的な力を養うのに良さそうです。
また、同じ課題で各自が実装すると知らなかったメソッドや書き方に出会えたり、比較して議論もしやすいです。

再実装しまくって君だけのRubyを作ろう!

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

最小限の手間でMac上にOACISを導入する方法

この記事ではMac上で最小限の手順でOACISの環境をセットアップする手順について説明します。

OACISは様々な環境で動作する様に汎用的に作られている一方で、Linuxの知識がないとセットアップが難しい部分もあります。ここではMac使い向けに「とにかくこの手順に従えばセットアップできる」という方法をお伝えします。
詳細が知りたくなったら公式ドキュメントを参照してください。
ここでは2020年1月時点で最新のv3.7.0をインストールします。

手順は以下の通り。

  1. Rubyをインストールする
  2. MongoDBとRedisをインストールする
  3. localhostに対してSSHログインできるように設定する
  4. OACISのソースコードをダウンロードする
  5. OACISを起動する
  6. OACISにlocalhostを登録する

Rubyをインストールする

rbenvを使ってインストールします。rbenv自体のインストール手順はrbenvのreadme参照してください。
rbenvをインストール後は、rubyをインストールします。

rbenv install 2.6.5 && rbenv global 2.6.5
gem update bundler
rbenv rehash

MongoDBとRedisをインストールする

homebrewを使ってMongoDBをインストールします。
詳細はMongoDBの公式ドキュメントを参照してください。

brew tap mongodb/brew
brew install mongodb-community@4.2
brew services start mongodb-community@4.2

同様にhomebrewを使ってRedisをインストールします。

brew install redis
brew services start redis

localhostに対してSSHログインできるように設定する

「システム環境設定」-「共有」を開き、「リモートログイン」にチェックを入れます。
場合によっては「ファイアウォールによりブロック」と表示されているかもしれませんが問題ありません。

image.png

続いてパスワード無しでログインできるように鍵認証の設定をします。

ssh-keygen -t rsa -N '' -f ~/.ssh/id_rsa
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys

これで自分自身の端末(localhost)にパスワード無しでログインできるようになります。
ここまでの設定がうまくいっていれば、以下のコマンドを実行してホームディレクトリのパスが表示されることを確認します。

ssh localhost pwd

OACISのソースコードをダウンロードする

OACISのソースコードをダウンロードします。

git clone --recursive -b master https://github.com/crest-cassia/oacis.git
cd oacis
bundle install

また、xsubというツールも同様にインストールします。

cd ~    # home directoryに移動
git clone https://github.com/crest-cassia/xsub.git
echo 'export PATH="$HOME/xsub/bin:$PATH"' >> ~/.bash_profile
echo 'export XSUB_TYPE="none"' >> ~/.bash_profile

OACISを起動する

先ほどダウンロードしたOACISのディレクトリに移動して、OACISを起動します

cd ~/oacis
bundle exec rake daemon:restart

これで http://localhost:3000 にブラウザからアクセスすると、OACISのトップ画面が表示されます。

OACISにlocalhostを登録する

OACISの画面からlocalhostをジョブの投入先として登録します。
画面上部の"Host"、"New Host"を選択し、以下の項目を入力します。

  • Name : localhost

これでlocalhostというホストが登録され、ジョブの投入先として選べるようになります。

image.gif

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

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #18.5 環境変数, Gmail送信設定編

こんな人におすすめ

  • プログラミング初心者でポートフォリオの作り方が分からない
  • Rails Tutorialをやってみたが理解することが難しい

前回:#18 EC2環境構築, Nginx+Puma+Capistrano編
次回:準備中

今回の流れ

  1. Railsの環境変数を設定する
  2. 本番環境でのGmailの送信を設定する

Railsの環境変数を設定する

Rails5.2以降の環境変数の設定には、credentials.yml.encを使います。
デフォルトで.gitignoreになり暗号化されているのでおすすめです。
以下のように設定します。

shell
EDITOR=vim rails credentials:edit
credentials.yml.enc
# aws:
#   access_key_id: XXXX
#   secret_access_key: XXXX

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: XXXX

おそらくこんな感じかと思うので、以下のように環境変数を追加します。

credentials.yml.enc
# 省略
a:
  b: bbb
  c: ccc
# 省略

以上で設定が終わりました。
試しにコンソールで値を引き出してみます。
値を引き出すには、以下のように行います。

shell
$ rails c
> Rails.application.credentials.a[:b]
=> "bbb"

この後のGmailの送信の設定には、このcredentials.yml.encを使います。

本番環境でのGmailの送信を設定する

前提として本番環境には、EC2を使っています。
またActionMailerの生成はすでに行なっているものとします。
(お済みでない方は#12などをご確認ください。)

それではGmailを送信の設定をします。
ここでの手順は以下の通りです。

  • Googleアカウントのアプリパスワードを有効にする
  • production.rbの設定を変更する
  • 環境変数を設定する

Googleアカウントのアプリパスワードを有効にする

お手持ちのGoogleアカウントのアプリパスワードを有効にします。
有効にするまでに、いくつかの手順を踏みます。

  1. googleアカウント → 画面左ダッシュボード『セキュリティ』 → Googleへのログイン『2段階認証プロセス』 → 有効にする
  2. googleアカウント → 画面左ダッシュボード『セキュリティ』 → Googleへのログイン『アプリパスワード』 → 16桁のパスワードをコピーする

これで次の手順に使うパスワードを手に入れました。

production.rbの設定を変更する

Gmail用に設定を変更します。

config/environments/production.rb
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.default_url_options = { host: Rails.application.credentials.host_server[:ip] }
  ActionMailer::Base.smtp_settings = {
    :address        => 'smtp.gmail.com',
    :port           => '587',
    :authentication => :plain,
    :user_name      => Rails.application.credentials.gmail[:user_name],
    :password       => Rails.application.credentials.gmail[:password],
    :domain         => 'gmail.com',
    :enable_starttls_auto => true
  }

先ほど見かけた、環境変数を引き出す記述があります。
最後はこれが動作するよう、環境変数を設定します。

環境変数を設定する

環境変数の設定です。
送信に使うアドレスやアプリパスワードはここに記述します。

credentials.yml.enc
host_server:
  ip: XX.XX.XX.XX # EC2のIPアドレス

gmail:
  user_name: hogehoge@gmail.com # Gmailアドレス
  password: hogehoge # コピーしたアプリパスワード

これで送信の設定が完了しました。

参考になりました↓
【Rails】メール送信設定 〜gmail利用〜


前回:#18 EC2環境構築, Nginx+Puma+Capistrano編
次回:準備中

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

リファクタリングでモブプロみたいなことをしたお話

弊社内でエンジニア3人が集まって楽しくリファクタリングをした話です。

背景

社内のプロダクトで「手が空いてるならリファクタリングやってくれると嬉しい。手が足らない」というようなSOSを受け取り、
そのプロダクトをある程度知っている人 1人 + 知らない人 2人 の3人でリファクタリング部隊を作ってリファクタリングしていこうという話になりました。

今回は、モブプロ「みたいなこと」です。厳密なモブプロではないです。
なので、

  • ドライバーも考える (みんなで考える)
  • ドライバーは変わらない

という感じでゆるく進行しました。

プロダクトについて

Railsで動いています。Rails3から始まり、今Rails5です。

  • 7年ぐらい前から動いているプロダクト
  • 新しく書いたところは新しく、古いものは古いという歴史が積み重なっている
  • Rails3からのコードが残っている
  • Rails何それ?Ruby何それ?から始まったコードが存在している
  • ただファイルが分割されただけのコードがConcernsとして存在している
  • でもテストはちゃんと書かれている

という感じで、とても歴史のあるプロダクトです。

今回やった人について

A: このプロダクトわかる。設計強い。DDD好き。Ruby, Railsともに得意。
B: このプロダクトわからない。インフラ強い。Rubyそんなに得意じゃない。 (本人談)
C: このプロダクトわからない。設計好き。DDD好き。Ruby得意。型を欲しがる。

という感じで思想やスキルの差が若干ありつつ、

  • 仲が良いので思想のぶつけ合いになっても険悪にはなりづらい
  • 誰か一人が発言しづらいということはない
  • お互いの技術に信頼/リスペクトしている

というメンバーです。今回はBさんがドライバーをしました。

今回の流れ

実際にどんな感じでやったのか?ですが、基本は

  1. つっつき会
  2. メシ
  3. 修正会

という3つのフェーズで行いました。
会議室で大画面を使用してみんなで唸ります。

つっつき会

今回のメインはモブプロをやることではないので、まずはリファクタリング対象(ディレクトリ/ファイル単位)を決め、
コードを読んでみてよくなさそうなところをつっついて、方針決めをします。大体4時間ぐらい。

このつっつきはTODO: つっつき serviceとして切り出す というようなTODOコメントとして残し、コミットします。

今回は、以下のような観点でつっついていきました。

  • そのファイルの構造、ネームスペースが正しいか(分割されただけのファイルがconcernsとして存在しているため)
  • そのクラスの責務を逸脱したメソッドがあるか(↑と大体一緒)
  • 冗長(わかりづらい)な書き方をしていないか
  • これらを大幅な変更せず綺麗にできるか

この時点でも、3人分の目と知識があるので、誰かが「この書き方はなんだ?」「これどうなってるんだ」となっても、知ってる/わかってる人が説明するということもあり、一人でやるときよりもスムーズです。

また、

これらを大幅な変更せず綺麗にできるか

という判断も1人だと消極的な理由でやらないことを選択してしまいそうな部分も、誰かの「実は簡単に修正できるのでは」という知識の後押しにより、積極的な判断をすることもできます。もちろん、やらないという判断もあります。

特に、やるか/やらないか という部分以外にも判断材料を出せる人が増えるというのは多人数の有利な部分です。

メシ

ramen.jpg

修正会

実際につっつき会でつっついた部分をリファクタリングしていきます。大体2時間ぐらい
基本的につっつき会で方針が出ているのでその方針に沿ってリファクタリングするだけです。
とはいえ、この段階でも「あ、もっといい方法あるじゃん」というのはあるので、随時取り入れていきます。

KPT

Keep

  • 誰かの「これどうなるんだろう?」に対して「じゃあ、試してみるか」というラフな形でやってみることができる
  • コードではわからないお互いの思考フローが知れる
  • ドメインエキスパートが居ないことによる「名前から直感的に判断できるか」という部分に焦点をあてれる

Problem

  • 仲が良いからこそ、脱線すると脱線し続けそうになる
  • ドメインエキスパートが居ないので仕様がわからないのでパスというのがあった
  • 複数人いるのでガッツリ集中して書くというのは難しい

Try

  • ドメインエキスパートも呼んでみる
    • 仕様についてをサクッと聞けるので時間短縮になりそう
    • keepの部分とコンフリクトしそう
  • 人数を増やしてみる
    • 人が増えれば集合知というのは単純な加算になるのか

終わりに

ペアプロ、モブプロは人と人の知識をリアルタイムでつなぎ合わせる というのがコアだと思うので、
「ドライバーは10分」「ナビゲーターの指示に従う」といった感じで厳密にやる必要は別になく、人が集まってみんなでコードに向かうというのは大事だと感じました。
特にkeepでも書いた、

コードではわからないお互いの思考フローが知れる

というのはお互いに勉強になります。
また、エンジニア間のコミュニケーションも取れるのでお互いに刺激になり、メインの業務にもハリが出ると思います。

更に、人間誰しも見落としや勘違いもあるので、単純に人数が増えることにより、見落としや勘違いなどをし続けることがなくなりやすいです。 (ダブルチェックと一緒で単純に増えればいいわけではないですが)

と、いう感じで、定期/不定期に限らずこういうことをやるのはいい感じだと思います。

おわり。

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

【Rails】I18n.tはtに省略できるんじゃない、省略したほうが良い

I18n.tの省略記法

まだなんにもわかっていないRails触り始めて1週間くらいの頃、
「RailsはViewファイル内ではI18n.t('hoge')って書かなくてもt('hoge')って書けるよ、
先輩たちもそう書いてるから合わせてね」
って言われてふむふむなるほど、
よくわからんがviewでは省略できるのかってなった経験はありませんか?

実はI18n.tをtにすると文字が4文字節約できるってレベルではない変化が起きていた

つまりどういう事か

詳しい実相はこの辺
TranslationHelperにtが実装されていたんですね

rails/actionview/lib/action_view/helpers/translation_helper.rb
def translate(key, options = {})
  options = options.dup
  if options.has_key?(:default)
    remaining_defaults = Array.wrap(options.delete(:default)).compact
    options[:default] = remaining_defaults unless remaining_defaults.first.kind_of?(Symbol)
  end

  # If the user has explicitly decided to NOT raise errors, pass that option to I18n.
  # Otherwise, tell I18n to raise an exception, which we rescue further in this method.
  # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default.
  if options[:raise] == false
    raise_error = false
    i18n_raise = false
  else
    raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations
    i18n_raise = true
  end

  if html_safe_translation_key?(key)
    html_safe_options = options.dup
    options.except(*I18n::RESERVED_KEYS).each do |name, value|
      unless name == :count && value.is_a?(Numeric)
        html_safe_options[name] = ERB::Util.html_escape(value.to_s)
      end
    end
    translation = I18n.translate(scope_key_by_partial(key), **html_safe_options.merge(raise: i18n_raise))
    if translation.respond_to?(:map)
      translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
    else
      translation.respond_to?(:html_safe) ? translation.html_safe : translation
    end
  else
    I18n.translate(scope_key_by_partial(key), **options.merge(raise: i18n_raise))
  end
rescue I18n::MissingTranslationData => e
  if remaining_defaults.present?
    translate remaining_defaults.shift, options.merge(default: remaining_defaults)
  else
    raise e if raise_error

    keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
    title = +"translation missing: #{keys.join('.')}"

    interpolations = options.except(:default, :scope)
    if interpolations.any?
      title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(", ")
    end

    return title unless ActionView::Base.debug_missing_translation

    content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
  end
end
alias :t :translate

ずいぶん色々やってますね

ざっくりいうとI18n.tに引数を渡す前に該当するロケールが存在しなかった場合の保険とか、
挙動にまつわるオプションの分岐とかががっつり書かれているわけですね

というわけで今後I18n.tってviewに書くプルリクエストが来たら修正してもらいましょう

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

Ruby 変数に格納された文字列の開始文字数と終了文字数を指定して表示する

目的

  • 変数に格納された文字列の開始文字数と終了文字数を指定して表示する方法をまとめる

書き方の例

  • 下記に変数Aに格納された文字列を開始文字数と終了文字数を指定して表示する方法を記載する。

    puts 変数A.slice(開始文字数..終了文字数)
    

より具体的な書き方の例

  • 変数todayに格納された文字列の0文字目から10文字目までを表示する方法を記載する。

    today = "2020-02-04 09:01:35"
    puts today.slice(0..10)
    2020-02-04
    
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む