- 投稿日:2019-10-11T22:32:13+09:00
rubyXL で印刷範囲を設定すると死ぬ問題
環境
rubyXL 3.4.6
なんでエクセルファイルが死んでしまうん?
印刷範囲は
workbook.xml
内のdefinedNames
要素に、定義名が_xlnm.Print_Area
として全シート分定義されます<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <workbook> <definedNames> <definedName name="_xlnm.Print_Area" localSheetId="0">シート1!$A$1:$I$53</definedName> <definedName name="_xlnm.Print_Area" localSheetId="1">シート2!$A$1:$I$53</definedName> <definedName name="_xlnm.Print_Area" localSheetId="2">シート3!$A$1:$I$53</definedName> <definedName name="_xlnm.Print_Area" localSheetId="3">シート4!$A$1:$I$53</definedName> <definedName name="_xlnm.Print_Area" localSheetId="4">シート5!$A$1:$I$53</definedName> </definedNames> </workbook>また、
app.xml
のTitlesOfParts
要素に、 シート名!印刷範囲 の形で記録されます。<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Properties> <TitlesOfParts> <vt:vector baseType="lpstr" size="10"> <vt:lpstr>シート1</vt:lpstr> <vt:lpstr>シート2</vt:lpstr> <vt:lpstr>シート3</vt:lpstr> <vt:lpstr>シート4</vt:lpstr> <vt:lpstr>シート5</vt:lpstr> <vt:lpstr>シート1!Print_Area</vt:lpstr> <vt:lpstr>シート2!Print_Area</vt:lpstr> <vt:lpstr>シート3!Print_Area</vt:lpstr> <vt:lpstr>シート4!Print_Area</vt:lpstr> <vt:lpstr>シート5!Print_Area</vt:lpstr> </vt:vector> </TitlesOfParts> </Properties>しかし、悲しいことに rubyXL では
definedName
要素のname
属性の値がそのままTitlesOfParts
に設定されてしまうのでした。<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Properties> <TitlesOfParts> <vt:vector baseType="lpstr" size="10"> <vt:lpstr>シート1</vt:lpstr> <vt:lpstr>シート2</vt:lpstr> <vt:lpstr>シート3</vt:lpstr> <vt:lpstr>シート4</vt:lpstr> <vt:lpstr>シート5</vt:lpstr> <vt:lpstr>_xlnm.Print_Area</vt:lpstr> <vt:lpstr>_xlnm.Print_Area</vt:lpstr> <vt:lpstr>_xlnm.Print_Area</vt:lpstr> <vt:lpstr>_xlnm.Print_Area</vt:lpstr> <vt:lpstr>_xlnm.Print_Area</vt:lpstr> </vt:vector> </TitlesOfParts> </Properties>ではどうするか
モンキーパッチ!
require 'rubyXL' module RubyXL::DocumentPropertiesFilePatch XLNM_PATTERN = /^_xlnm\./ def before_write_xml workbook = root.workbook self.heading_pairs = RubyXL::VectorValue.new(:vt_vector => RubyXL::Vector.new(:base_type => 'variant')) self.titles_of_parts = RubyXL::VectorValue.new(:vt_vector => RubyXL::Vector.new(:base_type => 'lpstr')) worksheets = chartsheets = 0 workbook.worksheets.each { |sheet| add_part_title(sheet.sheet_name) case sheet when RubyXL::Worksheet then worksheets += 1 when RubyXL::Chartsheet then chartsheets += 1 end } add_parts_count('Worksheets', worksheets) if worksheets > 0 add_parts_count('Charts', chartsheets) if chartsheets > 0 if workbook.defined_names then add_parts_count('Named Ranges', workbook.defined_names.size) # workbook.defined_names.each { |defined_name| add_part_title(defined_name.name) } xlnm_defined_names = workbook.defined_names.select {|v| v.name =~ XLNM_PATTERN } defined_names_without_xlnm = workbook.defined_names - xlnm_defined_names if xlnm_defined_names.present? then xlnm_defined_names.each do |defined_name| sheet_name = /^(.*)!/.match(defined_name.reference)&.values_at(1)&.first local_sheet_id = workbook.worksheets.index {|v| v.sheet_name == sheet_name }&.to_i if sheet_name.present? if local_sheet_id.present? then defined_name.local_sheet_id = local_sheet_id name = defined_name.name.gsub(XLNM_PATTERN, "#{sheet_name}!") add_part_title(name) else defined_names_without_xlnm << defined_name end end end defined_names_without_xlnm.each { |defined_name| add_part_title(defined_name.name) } end true end end RubyXL::DocumentPropertiesFile.prepend RubyXL::DocumentPropertiesFilePatchおまけ
下記は、実ファイルを気合で書き換えた場合の diff です
@@ -54,6 +54,8 @@ module RubyXL end private :add_part_title + XLNM_PATTERN = /^_xlnm\./ + def before_write_xml workbook = root.workbook @@ -75,7 +77,28 @@ module RubyXL if workbook.defined_names then add_parts_count('Named Ranges', workbook.defined_names.size) - workbook.defined_names.each { |defined_name| add_part_title(defined_name.name) } + # workbook.defined_names.each { |defined_name| add_part_title(defined_name.name) } + + xlnm_defined_names = workbook.defined_names.select {|v| v.name =~ XLNM_PATTERN } + defined_names_without_xlnm = workbook.defined_names - xlnm_defined_names + + if xlnm_defined_names.present? then + xlnm_defined_names.each do |defined_name| + sheet_name = /^(.*)!/.match(defined_name.reference)&.values_at(1)&.first + local_sheet_id = workbook.worksheets.index {|v| v.sheet_name == sheet_name }&.to_i if sheet_name.present? + if local_sheet_id.present? then + defined_name.local_sheet_id = local_sheet_id + + name = defined_name.name.gsub(XLNM_PATTERN, "#{sheet_name}!") + add_part_title(name) + else + defined_names_without_xlnm << defined_name + end + end + end + + defined_names_without_xlnm.each { |defined_name| add_part_title(defined_name.name) } end trueホントはプルリク出したい。
- 投稿日:2019-10-11T21:32:54+09:00
Railsチュートリアル 第9章<復習>
第9章の復習メモです。
個人的に重要と思ったことを書きます。前回と同様、以下3つの視点で書きます。
- 分かったこと
- 分からなかったこと
- 今回はスルーしたこと
分かったこと
永続セッション
前章では、一時cookieを使ったので、ブラウザを閉じると情報が消えてしまった。本章では、ブラウザを閉じても消えない永続cookieを使った。
前章の通りRailsでは、sessionメソッドを使うことで、一時cookieに情報を保存できる。sessionメソッドは、保存する際に情報を暗号化してくれるので、安全性が高い。
一方で、永続cookieの場合は、cookiesメソッドを使う。cookiesメソッドは、自動で暗号化する機能が無いため、安全性が低い。今回、永続cookieのセキュリティを強化するため、以下の対策を行った。
- cookieにユーザIDを保存する際、暗号化の処理を行う。
- ユーザIDに加えて、サーバ上でトークンを発行し、cookieに保存する。
今回、永続セッションを作成した手順は以下の通り。
- ランダムな文字列を生成し、これをトークンとする。
- ブラウザのcookiesにトークンを保存する。有効期限も設定しておく。
- トークンをデータベースに保存する。その際、ハッシュ値に変換する。
- ブラウザのcookiesに、暗号化したユーザーIDを保存する。
- 永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、トークンのcookiesがデータベース内のハッシュ値と一致することを確認する。
分からなかったこと、今回はスルーしたこと
- 永続セッション実装の詳細
今回は、セッションそのものの仕組みについて、理解することに努めました。コーディングは一通り実装してみて、流れは分かったかな、という感じです。
- テスト全般
参考
以下を参考にさせていただきました。
https://qiita.com/hot_study_man/items/147f8b767b4135fe6fe4
https://www.masalog.site/entry/2017/08/31/171712
- 投稿日:2019-10-11T21:09:22+09:00
Ruby ユーザ の 入力数値 を 変数 に 格納する
目的
- Rubyにてユーザからの入力数値を受け取り、変数に格納する方法をまとめる。
書き方の例
- 入力を受け取るにはメソッド
gets
を使用する。- 入力の改行もなくして変数に格納したいためオプション
chomp
を付ける。- 入力が数値であるため、さらにオプション
to_i
を付ける。- 下記に処理を記載する。
変数 = gets.chomp.to_iより具体的な例
- 入力された数値をコンソールに出力する処理を考える。
- コンソールには「好きな数値を入力してください。」 → 入力待ち → 「入力された数値は○○です。」と表示されるようにする。
- 入力された数値は変数
input
に格納する。- 下記に処理を記載する。
puts "好きな数値を入力してください。" input = gets.chomp.to_i puts "入力された数値は#{input}です。"
- 投稿日:2019-10-11T20:47:00+09:00
記事投稿のテストです。
- 投稿日:2019-10-11T20:37:00+09:00
EC2のRuby/Rails環境構築中のwe're sorry, but something went wrongでハマった話
こんばんは!ポートフォリオをいよいよデプロイしようとした時にハマったエラー
we're sorry, but something went wrong
[ec2-user@ip-172-31-23-189 ~]$ unicorn_rails -c config/unicorn.rb -E production -Dで問題なくunicornが走ったと思い
http://<ElasticIP>:30000
にアクセスするとwe're sorry, but something went wrongいつも通り再起動
[ec2-user@ip-172-31-23-189 ~]$ sudo shutdown -r now [ec2-user@ip-172-31-23-189 ~]$ sudo service mysql stop [ec2-user@ip-172-31-23-189 ~]$ sudo service mysql start [ec2-user@ip-172-31-23-189 ~]$ cd /var/www/sample-app [ec2-user@ip-172-31-23-189 sample-app]$ cd /var/www/sample-app [ec2-user@ip-172-31-23-189 sample-app]$ unicorn_rails -c config/unicorn.rb -E production -Dまだnginxの設定していない状態だけど、いつもだとこれでうまくいくはず。だけど今回はダメだった。
production.rbのログ確認
エラーの糸口が掴めずメンターに聞くと教えてくれた
[ec2-user@ip-172-31-23-189 sample-app]$ cd log [ec2-user@ip-172-31-23-189 log]$ cat production.rb
catコマンド
でログの内容を確認していくと見覚えのあるエラーを発見。ActionView::Template::Error
解決
トップページに設置している画像が存在してないよってことでした。
<%= image_tag("hoge.png"), class:"hoge" %>
hoge.png
はapp/assets/images配下に置いてた画像でgitignore
してました。ひとまず
img src
に書き換える<img src="http://hogehoge.png" alt="" class-"hoge">これでもう一度、unicornを再起動して走らせたら無事にアクセスできました。
アセットファイルをコンパイル
[ec2-user@ip-172-31-40-237 sample-app]$ rails assets:precompile Yarn executable was not detected in the system. Download Yarn at https://yarnpkg.com/en/docs/install怒られた。
[ec2-user@ip-172-31-40-237 sample-app]$ npm install yarn -g //これでも怒られたら [ec2-user@ip-172-31-40-237 sample-app]$ sudo npm install yarn -g //これでいけるはず [ec2-user@ip-172-31-40-237 sample-app]$ rails assets:precompile ~~ success Saved lockfile. //unicornをkill -9 [pid]してunicorn再起動 [ec2-user@ip-172-31-40-237 sample-app]$ RAILS_SERVE_STATIC_FILES=1 unicorn_rails -c config/unicorn.rb -E production -D無事にレイアウトも綺麗になりました。
参考
EC2にてnpm install yarn -gが失敗する
https://qiita.com/tsumita7/items/a40a367088018b5bbe33まとめ
2度目のデプロイですが、やはり一筋縄ではいきません。ハマったエラーはメモって次回はハマらないようにしていきます。
終わり
- 投稿日:2019-10-11T20:11:45+09:00
【初心者向け】PG二年目の私がコーディングでもっと早くから気をつけたかったこと
はじめに
コードを書く時、「こういう時ってどう書いたらいいの?」「そもそも何が綺麗なコードなの?」と悩んだことはありませんか?
私はあります。
この記事はかの有名なリーダブルコード
を読んだ際、私自身が出来ていなかったこと、意識しなければならないと思ったことを纏めたものです。
自分なりに纏めていますので、おかしい記述等もあるかもしれませんが、その際はご指摘ください。命名規則について
具体的な名前を付ける
- 誰が見ても、その名前だけで以下の2点が分かるような名前を付ける。
- 何をするメソッドか
- 何が格納されている変数か
- 以下の項目を意識すると具体的な名前になりやすい。
- 動詞を使うときは、より明確な動詞を選ぶ。汎用的なものはなるべく使わない。(類語を調べ、その中から選ぶといい)
- 私はこちらの記事を参考にするようにしています。
- 値の単位を付ける。(時間、サイズなど)
- 「どんな」を意識する。具体的にいうと形容詞や中身を連想できる名詞を添える。
records
よりもmessages
、 ハッシュを格納する変数であれば○○_hash
の方が直感的に中身の想像が出来る。リファクタリングするときに意識すること
ベースとして意識することについて
- 同じことは何度も書かない。
- 同じことを何度も書く必要がある場合はメソッドに分ける、変数に格納する
- 並び順に意味を持たせる。
- 複数変数が出てくる場合、定数が複数出てくる場合はその並びに意味を持たせる。
- 例:アルファベット順、画面での並び順、ファイルへの出力順など
- 行数が多い場合は適度に改行を挟み、見やすくする。
- 例:処理のまとまり単位で改行、など。
コメントについて
書いてはいけないコメント
- コードを見れば誰でも分かること
- メソッド名や変数名について説明するようなコメント(それは付けている名前が悪い)
不具合があるコードに付けるコメント
- 以下のprefixを付ける
prefix 意味 TODO: あとで直す FIXME: 既知のバグがあるコード HACK: あまり綺麗じゃない XXX: 大きな問題がある ※
prefix
が大文字の場合は大きな問題、小文字の場合は小さな問題という意味合いになります。
※TODO(ラテ太郎):
みたいな書き方をします。コメントを書いてもいい場所
- 読み手が一見で分からない or 疑問を持ちそうなところ
- なんで?これは何?と思われるようなコードであるならば、コメントの記載が必要です。
- 間違えて使われる可能性がありそうなところ
簡潔なコメントの書き方
その
のような指示語は避ける。(自分が思っているものと違う認識をされる可能性がある為、具体的に書くのが良いです。)かどうか
という表現は使わない。
- どうなったらどう、という書き方を心がける
×: Aかどうか
○ : AならばB
- 情報密度の高い言葉を使う。
- 自分の伝えたい内容が集約された言葉を選ぶことを意識すればコメントが簡潔になります。
- 実例を書く
ループ・ロジックの単純化
条件式
- 条件は肯定が正義。(わかりやすい)
- ド・モルガンの法則を駆使してなるべく肯定的な条件になるようにする。否定の否定、は使わない。悪。
- 目立つ条件、重要な条件から書く。
- 何でもかんでも三項演算子を使わない(わかりにくくなるため)
- 早期リターンできる時は早期リターンする(早期リターンすることでネストが減る。)
まとめ
やっぱり命名って難しい!!!!!
まずは上記のような、常に付きまとってくる問題と戦いながら、可読性の高いコードを書けるようになりたいと思います。参考
- 投稿日:2019-10-11T20:09:05+09:00
データを削除しようとしたらそのテーブルに主キーがなくて消せないんだけど
冒頭
LaravelからRailsに改修をする案件をしていたときのことで、
既存のDBがあるためそれをself.table_name
で指定してあげて使えるようにしているのですが、
データを論理削除する際に以下の問題にぶち当たりました。問題のエラー
ActiveRecord::StatementInvalid (Mysql2::Error: Unknown column 'テーブル名.' in 'where clause')
え、なんでカラムがないの?
と思いSQLを出力させたところ、UPDATE `テーブル名` SET `テーブル名`.`deleted_at` = '2019-10-11 18:12:27' , `テーブル名`.`updated_at` = '2019-10-11 18:12:27' WHERE `テーブル名`.`` IS NULLといった状態に。
DB設計書を確認したところ、どうやら主キー(Primary Key)がこのテーブルないとさ。
なるほど、そりゃ主キーないんだからカラムが空なわけ。調査
where句を別のものに指定できないものかと調べたらRails6から実装されているこんなものを見つけました。
# Finds and destroys all records matching the specified conditions. # This is short-hand for <tt>relation.where(condition).destroy_all</tt>. # Returns the collection of objects that were destroyed. # # If no record is found, returns empty array. # # Person.destroy_by(id: 13) # Person.destroy_by(name: 'Spartacus', rating: 4) # Person.destroy_by("published_at < ?", 2.weeks.ago) def destroy_by(*args) where(*args).destroy_all endだけどこれ結局のところ
delete_all
使ってるんで意味ないやん。
あ" き" ら" め" た"対応
生SQLをつかうことにした。
sql = <<-SQL UPDATE `テーブル名` SET `テーブル名`.`deleted_at` = '#{Time.current}' , `テーブル名`.`updated_at` = '#{Time.current}' WHERE `テーブル名`.`カラム名` = 'XXX' SQL con = ActiveRecord::Base.connection con.execute(sql)Railsつよつよの方へ
もしなにかしら方法をしっていたら教えてください?♂️
いや、普通に主キー作ればいいんだけどさ...
- 投稿日:2019-10-11T19:04:06+09:00
【Ruby】Zlibを使用して文字列を圧縮・展開する
事前準備
特に必要なし。
サンプルコード
# frozen_string_literal: true require 'zlib' # 圧縮前の文字列 str = 'Hello World' * 10_000 # 圧縮レベルを指定 # level = Zlib::NO_COMPRESSION # 圧縮しない # level = Zlib::BEST_SPEED # 速度優先で圧縮率は低い level = Zlib::BEST_COMPRESSION # 圧縮率優先で速度は遅い # 圧縮 deflated_str = Zlib::Deflate.deflate(str, level) # 展開 inflated_str = Zlib::Inflate.inflate(deflated_str) puts str.bytesize # => 110000 puts deflated_str.bytesize # => 252 puts inflated_str.bytesize # => 110000 puts str == inflated_str # => true参考
- 投稿日:2019-10-11T18:53:40+09:00
「Ruby初心者向けのプログラミング問題を集めてみた」の電話帳問題解いてみた。
はじめに
この記事は「Ruby初心者向けのプログラミング問題を集めてみた」の電話帳問題を解く過程から、先輩のレビューをいただき、振り返るところまでを纏めた記事です。
備忘録・振り返り的な要素が強い為、フランクな書き方をしておりますが、大目に見ていただければと思います。軽い読み物だと思って読んでください。
また解き方に稚拙な箇所もあるかと思いますがご容赦ください。指摘等は大歓迎です。励みになります。登場人物
- 私
- 社会人2年目PG。Rubyでコーディングし始めて1年とちょっと。うっかりポンコツ。最近Ruby Goldを取得した。
- ラテ太郎(アイコン参照)
- 私の心の中に住んでいる妖精。
白黒つけないいいやつ。ゆるい見掛けによらずしっかりしている。最後の砦。- タピオカ先輩
- ラテ太郎の先輩妖精。コーディングが得意。
フェーズ1 「考える・自力で解く」
実際に出題された問題
# NameIndex ## 問題 - カタカナ文字列の配列を渡すと、ア段の音別にグループ分けした配列を返すプログラムを作成せよ。 - 各要素は 50 音順にソートもすること。 ## 例 - IN: `['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ']` - OUT: `[ ['ア', ['イトウ']], ['カ', ['カネダ', 'キシモト']], ['ハ', ['ハマダ', 'ババ']], ['ワ', ['ワダ']] ]` ## 提出時、以下全ての条件を満たしていること - `RSpec` でエラーが発生していないこと - `RuboCop` で警告が出ていないこと - `Coverage` が `100%` であること考えたこと
私「なるほどなるほど、
辞書みたい
な感じでそれぞれの名前を纏めてあげればいいんだ。out
の形ってなんだかgroup_by
した時の形に似てない?(※この時の私は空前のgroup_by
ブーム)同じ感じ
でやれたらいいのにな〜。」ラテ「こんなイメージで合ってるかな?」
class NameIndex def self.create_index(names) names.group_by { |name| name.chr } end end私「そうそう、そんな感じ。私は
group_by
のところをgroup_by(&:chr)
って書くかな。(※書かないとRuboCopに怒られる。)」class NameIndex def self.create_index(names) names.group_by(&:chr) end end names = ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ'] NameIndex.create_index(names) =>{"キ"=>["キシモト"], "イ"=>["イトウ"], "バ"=>["ババ"], "カ"=>["カネダ"], "ワ"=>["ワダ"], "ハ"=>["ハマダ"]}私「これを
to_a
で配列にしたらイメージに近くない?なんかイメージに近い気がする!……でもこれって並び替えも必要だよね。うーん、各要素はソートすること
って書いてあったよなぁ。各要素をソートする……。」※シンキングタイム
私「最初から配列の中身ソートしとけばええやんけ…………………………………………………………………………。」
ラテ「気付くのおっそ…………。」
class NameIndex def self.create_index(names) names.sort.group_by(&:chr).to_a end end names = ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ'] NameIndex.create_index(names) =>[["イ", ["イトウ"]], ["カ", ["カネダ"]], ["キ", ["キシモト"]], ["ハ", ["ハマダ"]], ["バ", ["ババ"]], ["ワ", ["ワダ"]]]私「これです!!!!!!!!!!!!」
私「ちょっとどうしよう凄いしっくりきた。」
私「初めて一行で書けたのでは?」
私「やった〜〜〜〜〜〜〜〜〜〜〜嬉しい!」
私「RSpec書いて、時間を置いてから見直して、リファクタリングできるところあったらリファクタリングしよ!」・
・
・
・
・
・
・
・ラテ「お判りいただけただろうか…………。」
ラテ「こいつは凄い愚かなヤツです。どの辺が愚かかというと、
自分もやればできるじゃん
という気持ちに慢心しているところが愚かなのです。見直しは重要だということは小学生でも知っているのです。勿論、こいつにもそういう気持ちはあったのだと思うのです。あるからこそ時間を置いてから見直して
なんて言ってるのです。けれど、時間を置きすぎてはならないのです。なぜって時間には限りがあるから…………。」・
・
・
・
・
・
・
・
・
・
・
・私「待って?」
私「これ、ア
とかハ
で纏まってなくない?」
私「辞書みたいなイメージ
が先行してたけどこれ問題と違う
よね?」私「(大混乱)」
私「落ち着け、落ち着け私。きっと
ここまでの課程で考えてきたこと
と今までの経験が私を助けてくれるはず。」私「グループ化するっていう発送は悪くないはず。
キーがインデックス、valueがそのインデックスに含まれる文字、みたいなハッシュ
を作ったらいいんじゃないか?マッピング的な……ちょっと違う気がするけど……。」私「あ〜お、って
範囲オブジェクト
でいける?数字とアルファベット以外でもいける?ええい、ままよ!食らえ!」('ア'..'オ').to_a => ["ア", "ィ", "イ", "ゥ", "ウ", "ェ", "エ", "ォ", "オ"]私「すっっっっっっっっっっっっご」
私「まじか……いやまあそうよな……できるよな……できるんか……凄いなRuby……。」
私「待てよ、カタカナだったらあれよな、
ヴ
とかあるよな。え、ヴァネッサとか出てくるかな電話帳。いやでもこれ苗字だけとか書いてないしな。友達にヴァネッサとかおるかもしれんし……ヴァネッサだけ行き場ないとか可哀想よな……。」ラテ「そうして出来上がった定数がこれ。」
INDEX = { 'ア': ('ア'..'オ').to_a << 'ヴ', 'カ': ('カ'..'ゴ').to_a, 'サ': ('サ'..'ゾ').to_a, 'タ': ('タ'..'ド').to_a, 'ナ': ('ナ'..'ノ').to_a, 'ハ': ('ハ'..'ボ').to_a, 'マ': ('マ'..'モ').to_a, 'ヤ': ('ヤ'..'ヨ').to_a, 'ラ': ('ラ'..'ロ').to_a, 'ワ': ('ワ'..'ン').to_a }.freeze私「
ン
っている???」
私「いや、ンダホさんとかおるかもしれんし……」私「入れよう」
私「問題はここからよな」
私「ア
はア
のグループ、ってグループ分けしたい。」
私「グループ分けしたいけどどうやってメンバー選出する……?」※シンキングタイム
私「メンバー選出といえば
select
やんけ…………。」
私「さっき作った範囲の中に頭文字が当てはまる子(文字列)を選出してあげたらいいんじゃん?include?
でそれはチェックできる……で、それをうまいこと配列にして……なんかやっと見えてきたぞ…………。」class NameIndex INDEX = { 'ア': ('ア'..'オ').to_a << 'ヴ', 'カ': ('カ'..'ゴ').to_a, 'サ': ('サ'..'ゾ').to_a, 'タ': ('タ'..'ド').to_a, 'ナ': ('ナ'..'ノ').to_a, 'ハ': ('ハ'..'ボ').to_a, 'マ': ('マ'..'モ').to_a, 'ヤ': ('ヤ'..'ヨ').to_a, 'ラ': ('ラ'..'ロ').to_a, 'ワ': ('ワ'..'ン').to_a }.freeze def self.create_index(names) INDEX.map do |key, value| index_names = names.select { |name| value.include?(name.chr) } end end end私「そうそう、これで最初に想定してたみたいに
要素をソート
して。index_names
が空じゃなかったらインデックスをつけてあげればいけるんじゃないか。」class NameIndex INDEX = { 'ア': ('ア'..'オ').to_a << 'ヴ', 'カ': ('カ'..'ゴ').to_a, 'サ': ('サ'..'ゾ').to_a, 'タ': ('タ'..'ド').to_a, 'ナ': ('ナ'..'ノ').to_a, 'ハ': ('ハ'..'ボ').to_a, 'マ': ('マ'..'モ').to_a, 'ヤ': ('ヤ'..'ヨ').to_a, 'ラ': ('ラ'..'ロ').to_a, 'ワ': ('ワ'..'ン').to_a }.freeze def self.create_index(names) INDEX.map do |key, value| index_names = names.select { |name| value.include?(name.chr) }.sort [key.to_s, index_names] unless index_names.empty? end end end names = ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ'] NameIndex.create_index(names) =>[["ア", ["アマネ"]], ["カ", ["キシモト"]], nil, nil, nil, ["ハ", ["ハマダ", "ババ"]], nil, nil, nil, ["ワ", ["ワダ"]]]私「盲点」
私「うっそやん………そりゃそうだわ…………なんで気付かんのじゃ……。」
私「
compact
しよ………。」
私「ごちゃごちゃしてるし、names
が空だったら素直に早期リターンしよ」ラテ「そして完成したのがこれ。」
class NameIndex INDEX = { 'ア': ('ア'..'オ').to_a << 'ヴ', 'カ': ('カ'..'ゴ').to_a, 'サ': ('サ'..'ゾ').to_a, 'タ': ('タ'..'ド').to_a, 'ナ': ('ナ'..'ノ').to_a, 'ハ': ('ハ'..'ボ').to_a, 'マ': ('マ'..'モ').to_a, 'ヤ': ('ヤ'..'ヨ').to_a, 'ラ': ('ラ'..'ロ').to_a, 'ワ': ('ワ'..'ン').to_a }.freeze def self.create_index(names) return [] if names.empty? INDEX.map do |key, value| index_names = names.select { |name| value.include?(name.chr) }.sort [key.to_s, index_names] unless index_names.empty? end.compact end end私「あっ、あ……RSpecも書き直しやん…………。先輩ごめんなさい………。(15分オーバー)」
フェーズ1で学んだこと
- カタカナも範囲オブジェクトにできる。
select
を使えばグループ化は簡単- 慢心しない、簡単にできたと思った時ほど見直しはしっかり。
- 時間に余裕を持って作業する。
- 問題は定期的に読み直す。自分の認識が間違っていないか確認する。
- 方向性を見失わないようにテストファーストで作業をする。
フェーズ2 タピオカ先輩からレビューをもらう
この課題を解いた後、見守っていたタピオカ先輩からレビューをもらった。
タピ「レビューしたんだけどさ。」
私「はい。」
タピ「今回悪いところなかった。」
私「???」
私「またまたぁ」
タピ「いや本当。今回の評価ポイント(要件を満たせているか・可読性・テストコードの充実度)は満たせてる。」
私「(動揺)(困惑)」
タピ「とりあえずレビューしていこうか」ポイント1 injectで無駄なく回す
タピ「私さん、
map
compact
してたじゃん。」
私「はい。map
だとnil
が混在しちゃうので」
タピ「じゃあ、nil
が混ざらないようにループ回したらよかったんじゃない?」
私「アッ」
map compact
orselect map
で作れる配列はinject
で作れinject
で作れるならeach_with_object
にしなさい(RuboCopの好み)タピ「この記事が参考になる。」
タピ「今回の問題だと、nil
になるものののチェックで無駄に処理が走るよね?例えば、電話帳にはア行しか登録されてないのに、他の行についての処理も走る。ループでいうなら2回ループが走ってる。必要なものだけの配列を作りたいならinject
を使えば1回で済む。」
私「そっか……そっか……そうですね……(知識としては持っていたのに未だ使いこなせていないことに対する悔しさに襲われる)」ポイント2 冗長にならないようなコーディングをする
タピ「あとさ、定数のハッシュ。キーと配列の0番目の値が一緒なの、なんか冗長じゃない?」
私「確かに……。」
タピ「しかもハッシュのキー、to_s
してるじゃん、to_s
するくらいならロケット記法
で最初からキーを文字列にしておけばよかったんじゃない?」
私「確かに……。」
タピ「しかもこのキーって配列作るときに使ってるだけじゃん?」
私「そうですね。そのためだけに用意しちゃいました。そのキーでまとめなきゃ!!って気持ちが強くて、キーを。」
タピ「つまりこう定義したら万事解決だったってこと。」class NameIndex SYLLABARIES = [ ('ア'..'オ').to_a << 'ヴ', ('カ'..'ゴ').to_a, ('サ'..'ゾ').to_a, ('タ'..'ド').to_a, ('ナ'..'ノ').to_a, ('ハ'..'ボ').to_a, ('マ'..'モ').to_a, ('ヤ'..'ヨ').to_a, ('ラ'..'ロ').to_a, ('ワ'..'ン').to_a ].freeze class << self def create_index(names) names.sort.group_by(&method(:initial)).to_a end private def initial(name) SYLLABARIES.find { |values| values.include?(name[0]) }.first end end end私「これです」
私「こういうの書きたかったんです」
私「
group_by
使われてるし、メインのメソッドは一行で書かれてるし、超カッケー(配列か〜〜〜〜〜!!マッピングしなきゃって気持ちが強すぎた)」〜タピオカ先輩の解説タイム〜
タピ「例えば、ア〜オ+ヴの中に、名字の1文字目が含まれていたらア行に纏められるわけでしょ。
group_by
の中でアカサタナ〜が返れば今回の想定した配列が作れるじゃん。つまり、『名字の1文字目が
SYLLABARIES
の配列のどれかに含まれていたら、その配列の1文字目が返ればいい』ってことね。それが
initial
メソッド。」タピ「そのメソッドを
group_by
と合わせて実行すればいいだけ。今回は&method
を使って書いてみた。」私「(
group_by
とはこうやって使うのか)」タピ「どう?納得?」
私「納得しかないです……すげ……」
フェーズ2で学んだこと
group_by
はブロック内の返り値でグルーピングされる=>自分の思う返り値が返るようなメソッドを作って呼んであげればグルーピングはできる(今回はgroup_by
が使えた!)&method
を使うと短く書ける(参考: &演算子と、procと、Object#method について理解しなおす)- 冗長だと感じるところは徹底的に排除する(同じ文字がなんども出てくる、等。今回は定数の中身が冗長だった。)
最後に
やり方、考え方の方向性は合っていましたが、テクニカルな部分がまだまだだなと実感しました。しかしこれで
group_by
はマスターです。&method
はあまり使ったことがなかったので、使える場面では使っていきたいと思います。
また、自分の書くコードは冗長になりがちなので、「これって冗長じゃない?」という気持ちをもってコーディングすることを意識する必要があるなと感じました。
次の回ではもっといいコードを書けるように腕を磨いておきます。
- 投稿日:2019-10-11T18:31:19+09:00
Rubyのヒアドキュメントまとめ
- 投稿日:2019-10-11T18:23:06+09:00
Railsチュートリアル 第8章<復習>
第8章の復習メモです。
個人的に重要と思ったことを書きます。前回と同様、以下3つの視点で書きます。
- 分かったこと
- 分からなかったこと
- 今回はスルーしたこと
分かったこと
セッションについて
今回は、ログイン、ログアウトの機能を実装した。
- ユーザがログインし、ログアウトするまでの間、接続情報(ユーザID等)を保持する必要がある。
- 接続情報は、ブラウザとサーバ間にセッションを確立することで保持される。
- 情報が保持される場所は、ブラウザ上(HTTPプロトコルは状態を保持できないため、この方法を使う)。
- 保持する形式は、cookiesを用いる。これは、小さなテキストデータ形式である。
- 本章では、一時cookieを使う。この場合、ブラウザを閉じたら接続が破棄される。永続クッキーは9章で扱う。
- 一時cookiesに保存された情報は、sessionメソッドを用いて、Railsアプリケーションから参照できる(今回は、ログイン中のユーザIDを保存した)。
ルーティングの確認
rails routes
コマンドで、ルーティングの一覧が表示される。$ rails routes Prefix Verb URI Pattern Controller#Action root GET / static_pages#home help GET /help(.:format) static_pages#help about GET /about(.:format) static_pages#about contact GET /contact(.:format) static_pages#contact signup GET /signup(.:format) users#new login GET /login(.:format) sessions#new POST /login(.:format) sessions#create logout DELETE /logout(.:format) sessions#destroy users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroyflash.now
7章で学習したflash変数は、次のリクエストが終了するまでの間、表示される変数である。
しかし、今回はrenderメソッドを用いたので、flashが必要以上に残ってしまった(renderはリクエストが発生しない)。
この場合、flash.nowを使うと解決する。flash.nowは、現在のリクエストが終了するまでの間、表示される。flash.now[:danger] = 'Invalid email/password combination' # ← ここで使用 render 'new'以下を参考にさせていただきました。
https://qiita.com/shi-ma-da/items/ea433c337d2a691ff1bc
https://kossy-web-engineer.hatenablog.com/entry/2018/10/05/063957sessionメソッドの使い方
session情報の追加
app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id # ← session情報にユーザIDを追加 end endsession情報の削除
deleteメソッドを使う。
session.delete(:user_id)分からなかったこと、今回はスルーしたこと
- テスト全般
- アプリケーションの仕様の詳細
- 投稿日:2019-10-11T17:57:00+09:00
[初学者]rootメソッド
- 投稿日:2019-10-11T17:27:09+09:00
RubymineのdebuggerがRubygems 3.0以上で使えない問題
問題
- Rubymineでdebuggerを使おうとしたところ、関連gemのインストールを求められる
- インストールしようとしたところ、失敗する
エラーログ
Error running 'Api::V1::AccountsController... (1)' Failed to Install Gems. Following gems were not installed: /Applications/RubyMine.app/Contents/rb/gems/debase-0.3.0.beta8.gem: While executing gem ... (OptionParser::InvalidOption) invalid option: --no-rdoc /Applications/RubyMine.app/Contents/rb/gems/ruby-debug-ide-0.8.0.beta8.gem: While executing gem ... (OptionParser::InvalidOption) invalid option: --no-rdoc原因
- Rubymineがdeprecateなオプション
--no-rdoc
を渡している対応
--no-document
を渡すようにする- だけど、関連gemのRubymine指定バージョンはホスティングされていないので、いつもどおり叩いてもダメ
$ gem install --no-document ruby-debug-ide -v 0.8.0.beta8 #=> ERROR: Could not find a valid gem 'ruby-debug-ide' (= 0.8.0.beta8) in any repository #=> ERROR: Possible alternatives: ruby-debug-ide
- エラーログを見ると、ローカルにgemの指定バージョンを持っているので、それをインストールする
- gemのpathはエラーログからコピペする
$ gem install --no-document /Applications/RubyMine.app/Contents/rb/gems/ruby-debug-ide-0.8.0.beta8.gem $ gem install --no-document /Applications/RubyMine.app/Contents/rb/gems/debase-0.3.0.beta8.gem
- これでdebugger使えるようになる
参考
- 投稿日:2019-10-11T17:04:10+09:00
[初学者]ルーティングについて
目的
学習の備忘録と初学者の参考資料として投稿
ルーティング
ルーティングは、ブラウザから届いたリクエスト(HTTPメソッド+URL)に対して、コントローラーで定義したアクションを結びつけるルールです。
上記は参考例です。
ルーティングの確認
ターミナルで $ rails routes あるいは...ブラウザで http://localhost:3000/rails/info/routes と入力どちらでも確認出来ます。
HTTPメソッド
HTTPメソッドとは、「クライアントがサーバーにしてほしいことを依頼するための手段」のこと。
主に使うのは『GET』『POST』『PUT』『DELETE』の4つぐらいです。
それぞれの働きは『GET』 ・・・データを取得するときに利用する。
『POST』 ・・・サーバーにデータを送信する時に利用する。アカウント作成や投稿するなど新規作成で使われる。
『PUT』 ・・・サーバーにデータを送信する時に利用する。既存データの更新などで使われる。
『DELETE』・・・既存データを削除するときに利用する。
任意のアクションを呼び出したい時は
http://(ホスト名)/コントロール名/アクション名
で呼び出すことが可能です。
まとめ
今回は簡単なさわりだけを書いています。今後さらに深掘りして書いていきます。
今後も学習で気づきや参考になるものがあれば、アップしていきます。
もし参考になったらいいね!!よろしくお願いします
- 投稿日:2019-10-11T15:49:04+09:00
【Ruby】世界のナベアツのあのギャグで遊んでみよう
目次
- 1. この記事の狙い
- 2. ターゲット層
- 3. 実行環境
- 4. 世界のナベアツについて
- 5. Rubyコード
- 5-1. 期待する挙動
- 5-2. Comedianクラス実装
- 5-3. 実際に動かしてみよう
1. この記事の狙い
世の中には伊藤淳一さんのチェリー本(私も大変おせわになりました
)をはじめとした入門書、オンライン上でも無料の教材が揃っているので、インプットで不便することはない。
しかし、アウトプットをしないと自分の力に変換出来ないし忘れてしまう。なるべくなら肩の力を抜いて遊びながら基礎をおさらい出来たらあまり疲れないで済むで良い。
現実世界にはそんな遊びに適した材料が転がってるので、自分のリフレッシュも兼ねて思いつく限りで記事に書き起こしてみようというのが狙い。2. ターゲット層
- Rubyの入門書読んだけどまだ自力でスクラッチでコード書けない初心者の方
- 「こんなのもコードにしたら面白いんじゃない?」という好奇心に満ちた方(レベル問わず)
3. 実行環境
- ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-darwin18]
- MacOS Version 10.14.6
4. 世界のナベアツについて
- 元ジャリズムのコンビ芸人
- 「3の倍数と3のつく数字の時だけアホになる」ギャグで一斉風靡
- 現在は桂三度の芸名で落語家として活動中
- 詳しくはWikipediaを参照
5. Rubyコード
5-1. 期待する挙動
- オブジェクトを生成すると芸能プロダクションと芸名を添えて自己紹介する
- 芸名が「世界のナベアツ」の時だけ例のギャグを発動する
- アホの状態は数字に「!」を付けて表現
5-2. Comedianクラス実装
class Comedian def initialize(name:, age:, agency:) @name = name @age = age @agency = agency puts "こんにちは。私は#{@agency}で芸人をやっております#{@name}です。" return false unless @name == '世界のナベアツ' arr = 1.upto(40).map { |num| go_crazy(num) } puts arr.join(',') end def go_crazy(num) num % 3 == 0 || num.to_s.include?('3') ? "#{num}!" : num.to_s # データ型は全て文字列に統一 end end少し解説を入れる。
- initializeメソッドはオブジェクトを初期化するメソッド。
クラス.new
時に呼ばれる。なお、initializeメソッド自体はデフォルトでプライベートメソッド(外部からアクセス出来ない)ため、クラス.initialize
は不可。- 属性は
name
age
agency
の3つ。オブジェクト生成時の引数を単にComedian('芸名', 30, 'ナベプロ')
のようにすると何を表しているのか分かりづらい(可読性が低い)ので、キーワード引数を使用(今回はデフォルト値なし)。return false unless @name == '世界のナベアツ'
では、もし芸名が「世界のナベアツ」出ない場合、それ以降の処理が流れないようにfalse
を返している。この記法はGuard Clauseと呼ばれるもの。同じ処理をしようと以下のコードを書くとRubocopのテストに怒られる。if @name == '世界のナベアツ' arr = 1.upto(40).map { |num| go_foolish(num) } puts arr.join(',') end
- 例のギャグは「1から40までの数字」という範囲なので、mapを使ってループでギャグインスタンスメソッドを呼び、実行結果を配列に格納。そのまま出力すると配列の
[]
や文字列の""
が表示され美しくないのでjoinを使い,
を境に結合- 呼び出し元の処理の中身は、
num % == 0
で3で割り切れる数、num.to_s.include?('3')
で引数の数字を一旦文字列に変換し'3'という文字が含まれているか判定。いずれかの条件に当てはまる場合は引数の数字を文字列変換し「!」を付ける。それ以外の数字はそのまま integer で出力。5-3. 実際に動かしてみよう
irb(main):016:0> Comedian.new(name: '世界のナベアツ', age: 50, agency: '吉本興業') こんにちは。私は吉本興業で芸人をやっております世界のナベアツです。 1,2,3!,4,5,6!,7,8,9!,10,11,12!,13!,14,15!,16,17,18!,19,20,21!,22,23!,24!,25,26,27!,28,29,30!,31!,32!,33!,34!,35!,36!,37!,38!,39!,40 => #<Comedian:0x00007f90840aa6d8 @name="世界のナベアツ", @age=50, @agency="吉本興業">ちゃんとアホになった。
irb(main):018:0> Comedian.new(name: '千原ジュニア', age: 45, agency: '吉本興業') こんにちは。私は吉本興業で芸人をやっております千原ジュニアです。 => #<Comedian:0x00007f90839629e8 @name="千原ジュニア", @age=45, @agency="吉本興業">違う芸人ではギャグは発動しない。
- 投稿日:2019-10-11T14:44:32+09:00
Ruby 正規表現の学習2
正規表現の様々なパターンを使ってみる
先日は
subメソッド
、matchメソッド
の基本的な使い方を載せてみました。
今回は、正規表現の様々なパターンを使って少しだけ応用的な使用方法を使ってみたいと思います。
今回、使ってみるパターンは以下の3つ。
- 電話番号のハイフンを取り除く
- パスワードに英数字8文字以上という制約を設定
- メールアドレスからドメインの部分のみ抽出
1. 電話番号からハイフンを取り除く
ターミナルirb(main):001:0> tel = '090-1234-5678' => "090-1234-5678" irb(main):002:0> tel.sub(/-/,'') => "0901234-5678" # 最初のハイフンしか置換えされない irb(main):003:0> tel.gsub(/-/,'') => "09012345678"ポイント
- グローバルマッチの
g
sub
の前にg
が追加された、gsub
メソッドと言い、g
が意味するのは、グローバルマッチと言う。
文字列内で指定した文字が複数含まれている場合、その全てを置換えすると言う意味になる。
gsub
だけではなくsub
を使用した場合は、初めの1つだけ置換えされることになる。2. パスワードに英数字8文字以上という制約を設定
以下はパスワードに「Hoge1234」という大文字小文字を区別した英字と数字を使用して、
matchメソッド
を使用して記述してみる。ターミナルirb(main):001:0> pass = 'Hoge1234' => "Hoge1234" irb(main):002:0> pass.match(/[a-z\d{8,}/i) => #<MatchData "Hoge1234">ポイント
[a-z]
: 角括弧で囲まれた文字のいずれか1個にマッチ\d
: 数字にマッチ{n, m}
: 直前の文字が少なくとも n 回、多くても m 回出現するものにマッチi
: 大文字・小文字を区別しない検索[a-z] : 角括弧で囲まれた文字のいずれか 1個にマッチ
a~cの英字を抽出
「dog」にはa〜cのどの英字も含まれていないのでマッチしない。ターミナルirb(main):001:0> 'dog'.match(/[a-c]/) => nil\d : 数字にマッチ
\d
のd
は数字を表す。数字と表すd
のような文字を特殊文字と呼び、特殊文字を使用する場合は直前に\
を記述するというルールがある。[a-z\d]
は「英数字のいずれか1つにマッチ」という意味になる。ターミナルirb(main):001:0> 'I have 3 pens'.match(/\d/) => #<MatchData "3">{n, m} : 直前の文字が少なくとも n 回、多くても m 回出現するものにマッチ
少なくとも4回、多くても6回出現するものにマッチ
波括弧を使用することで文字数の制約を追加することができる。{4,6}
は、直前の文字が少なくとも下記の場合は4回多くても6回数字がマッチという意味になり、2回目のirbはマッチする数字がないのでnil
と返される。ターミナルirb(main):001:0> '12345678'.match(/\d{4,6}/) => #<MatchData "123456"> irb(main):002:0> '123'.match(/\d{4,6}/) => nili : 大文字・小文字を区別しない検索
i
オプションを加えることで大文字・小文字を区別しないで検索する。i
オプションをつけずに[a-z]
と小文字で記述すると大文字にマッチしなくなる。- 大文字・小文字の区別
ターミナルirb(main):003:0> 'Cat'.match(/cat/) => nil irb(main):004:0> 'Cat'.match(/cat/i) => #<MatchData "Cat">実践的な使用例
irbpass = 'Hoge1234' if pass.match(/[a-z\d]{8,}/i) // パスワード設定の処理 else puts 'パスワードの形式が間違えています。' endメールアドレスからドメインの部分のみ抽出
「hoge@sample-taka.com」というアドレスから「@sample-taka.com」の部分のみを取得したい場合。
ターミナルirb(main):001:0> mail = 'hoge@sample-taka.com' => "hoge@sample-taka.com" irb(main):002:0> mail.match(/@.+/) => #<MatchData "@sample-taka.com">ポイント
.
: どの1 文字にもマッチ+
: 直前の文字の 1 回以上の繰り返しにマッチ
.
どの1文字にもマッチハイフンやピリオドなど含めた全ての英数字において、どの1文字にもマッチする。
(例)
ターミナルirb(main):001:0> 'hoge'.match(/./) => #<MatchData "h">
+
直前の文字の 1 回以上の繰り返しにマッチ直前の文字が 1 回以上の繰り返しにマッチする。
(例)
ターミナルirb(main):001:0> 'aaabb'.match(/a+/) => #<MatchData "aaa">以上の例に沿ってみると
.+
は何かしたの文字が一回以上繰り返されるものにマッチする。- 先頭に
@
をつけることで「@から始まり、何かしらの文字が 1 回以上口返すものにマッチ」という意味になる。まとめ
パターン 意味 [a-z] 角括弧で囲まれた文字のいずれか 1 個にマッチ \d 数字にマッチ {n,m} 直前の文字が少なくとも n 回、多くても m 回出現するものにマッチ . どの 1 文字にもマッチ + 直前の文字の 1 回以上の繰り返しにマッチ まだまだ奥深い正規表現ですが以上のことだけは最低限おさえて置きたいと思います。。。
- 投稿日:2019-10-11T13:42:39+09:00
破壊的メソッド 非破壊的メソッド
- 大元も値を変更するかいなか
破壊的メソッドとは本体のデータを変更する事 - 非破壊的 string = "test code" string.slice!(0,4) p string → "test code" - 破壊的 string = "test code" string.slice!(0,4) p string → " code"
- 投稿日:2019-10-11T13:34:11+09:00
Railsで既存モデルをポリモーフィック化したら大変だった話
前提
社内でやろうとしてたことそのまま書けないので、
例えを探しましたが・・・わかりにくいかもですw
ポリモーフィックについては、こちらの記事がわかりやすかったです!感謝感激。モデルは下記とします。
モデル図 *()の中がモデル名
普通自動車(Car)-<部品(Part)>-人(Owner)Car
has_many :parts
has_many :owners, through: :partsPart
belongs_to :cars
belongs_to :ownersOwner
has_many :parts
has_many :cars, through: :parts目的は、
「ある人が所持している、ある普通自動車に搭載されている部品達をDBで管理できる」
とします。もうちょっと具体的にいうと、
「AさんのIDから次郎号という自動車を特定できて、
かつ次郎号に搭載されている部品(前輪・後輪とか)も把握できる」
みたいな感じです。異なるモデルも同一のモデルで管理したい
上記モデルをポリモーフィック可したらどうなるかというと
モデルはこんな感じになります。*()の中がモデル名
モデル図
なんかの乗り物-<部品(Part)>-人(Owner)なんか乗り物(ここではCarとShipとしておきます)
has_many :parts
has_many :owners, as: :vehicle, through: :partsPart
belongs_to :vehicles, polymorphic: true
belongs_to :ownersOwner
has_many :parts
has_many :cars, as: :vehicle, through: :parts
has_many :ships, as: :vehicle, through: :parts要するに
「ある人が所持している、ある乗り物に搭載されている部品達をDBで管理できる」何が良いの?となりますが
普通自動車以外の乗り物(バイク、船 etc...)の部品もPartモデルに突っ込めます!
Partモデルにvehicle_typeとvehicle_idというカラムを追加してあげることで
(vehicle_type = 'Car' とか vehicle_type = 'Ship'とか・・・)
Partからみたら、繋がりは一つに見えるのに色々な乗り物モデルと繋がれる
という状態になります。何が大変だったか
さて、本題ですが
1つ前のセクションでポリモーフィック化は完了したとします。
となると、既存のソース内で変更が必要になってきます。まず当時の私は、下記のクエリは問題ないと思っていました。(いつ使うんだこのクエリは!と言わないでくださいw)
「ある車に紐付いている、personを取り出す。条件は、男性で前輪を持っている人」
car = Car.find_by("123-456-789")
car.persons.joins(:parts).find_by('persons.sex = ? AND parts.name = ?', "male",
"前輪")これを叩くと、以下を含んだクエリが作成されます。
WHERE "parts"."car_id" = $1
ポリモーフィック化する以前は、Carモデルしかなかったのでこれで良かったのですが
ポリモーフィック後はvehicle_idというカラムに変わっているのでエラーになります。ここで注意すべきは、「じゃあparts.vehicle_idにすればいいのでは?」・・それでは、事足りないということです。
具体的には、先ほどのクエリをこんな風にすればOKです。
Person.joins(:parts).find_by('vehicle_id = ? AND vehicle_type = ? AND persons.sex = ? AND parts.name = ?', car.id, 'Car', "male", "前輪")
vehicle_idと合わせてvehicle_typeも引き合いに出すことで期待する値を確実に取ることができます。まとめ
既存モデルをポリモーフィック化すると、そのモデルの先で叩くクエリを書き直さなければならない場合があります。
その際は、XXX_typeというようにクエリへモデルのタイプ(String)を含める必要があります。おまけ: さらに抽象度を上げるには?
下記のクエリは、Carモデル用にハードコーディングされてます。
Person.joins(:parts).find_by('vehicle_id = ? AND vehicle_type = ? AND persons.sex = ? AND parts.name = ?', car.id, 'Car', "male", "前輪")
こんな風に変えるといい感じです!
Person.joins(:parts).find_by('vehicle_id = ? AND vehicle_type = ? AND persons.sex = ? AND parts.name = ?', vehicle.id, vehicle.class.to_s, "male", "前輪")
こうすれば、仮に変数vehicleにCarモデルが入ろうが、Shipモデルが入ろうが吸収してくれます。参考
- 投稿日:2019-10-11T12:43:51+09:00
Rails6 のちょい足しな新機能を試す94(ActiveModel falsy symbol編)
はじめに
Rails 6 に追加された新機能を試す第94段。 今回は、
ActiveModel falsy simbol
編です。
Rails 6 では、:false
など、false
を連想させる symbol が ActiveModel (ActiveRecord) の値として、 false として扱われるようになりました。Ruby 2.6.4, Rails 6.0.0 で確認しました。
なお、こちらは、Rails 5.2.4 以降でも同様の振舞いに変更されるものと思われます。
$ rails --version Rails 6.0.0プロジェクトを作る
$ rails new rails_sandbox $ cd rails_sandbox今回は Book モデルを作って rails console で確認してみます。
Book モデルを作る
title
とpublished
の2つの属性を持つ Book モデルを作ります。$ bin/rails g model Book title published:boolean
seed データを作る
seed データを作ります。
db/seeds.rbBook.create( [ { title: 'Agile Web Development with Rails 5.1', published: true }, { title: 'Agile Web Development with Rails 6', published: false } ] )rails console を実行する
rails console を使って確認してみます。
:"0"
,:f
,:F
,:false
,:FALSE
,:off
,:OFF
が falsy な値として扱われます。
ActiveModel::Type::Boolean::FALSE_VALUES
でどの値が falsy な値となるのか確認することができます。irb(main):001:0> ActiveModel::Type::Boolean::FALSE_VALUES => #<Set: {false, 0, "0", :"0", "f", :f, "F", :F, "false", :false, "FALSE", :FALSE, "off", :off, "OFF", :OFF}>:"0" の場合
irb(main):002:0> Book.where(published: :"0") Book Load (0.3ms) SELECT "books".* FROM "books" WHERE "books"."published" = $1 LIMIT $2 [["published", false], ["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 2, title: "Agile Web Development with Rails 6", published: false, created_at: "2019-09-28 00:34:09", updated_at: "2019-09-28 00:34:09">]>:f の場合
irb(main):003:0> Book.where(published: :f) Book Load (0.8ms) SELECT "books".* FROM "books" WHERE "books"."published" = $1 LIMIT $2 [["published", false], ["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 2, title: "Agile Web Development with Rails 6", published: false, created_at: "2019-09-28 00:34:09", updated_at: "2019-09-28 00:34:09">]>:F の場合
irb(main):004:0> Book.where(published: :F) Book Load (0.8ms) SELECT "books".* FROM "books" WHERE "books"."published" = $1 LIMIT $2 [["published", false], ["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 2, title: "Agile Web Development with Rails 6", published: false, created_at: "2019-09-28 00:34:09", updated_at: "2019-09-28 00:34:09">]>:false の場合
irb(main):005:0> Book.where(published: :false) Book Load (0.7ms) SELECT "books".* FROM "books" WHERE "books"."published" = $1 LIMIT $2 [["published", false], ["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 2, title: "Agile Web Development with Rails 6", published: false, created_at: "2019-09-28 00:34:09", updated_at: "2019-09-28 00:34:09">]>:FALSE の場合
irb(main):006:0> Book.where(published: :FALSE) Book Load (0.7ms) SELECT "books".* FROM "books" WHERE "books"."published" = $1 LIMIT $2 [["published", false], ["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 2, title: "Agile Web Development with Rails 6", published: false, created_at: "2019-09-28 00:34:09", updated_at: "2019-09-28 00:34:09">]>:off の場合
irb(main):007:0> Book.where(published: :off) Book Load (0.8ms) SELECT "books".* FROM "books" WHERE "books"."published" = $1 LIMIT $2 [["published", false], ["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 2, title: "Agile Web Development with Rails 6", published: false, created_at: "2019-09-28 00:34:09", updated_at: "2019-09-28 00:34:09">]>:OFF の場合
irb(main):008:0> Book.where(published: :OFF) Book Load (0.7ms) SELECT "books".* FROM "books" WHERE "books"."published" = $1 LIMIT $2 [["published", false], ["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 2, title: "Agile Web Development with Rails 6", published: false, created_at: "2019-09-28 00:34:09", updated_at: "2019-09-28 00:34:09">]>Rails 5.2.3 では
symbol (Rails 6 では falsy になる symbol) は truthy な値として扱われます。
試したソース
試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try094_falsy_symbol参考情報
- 投稿日:2019-10-11T10:35:07+09:00
kintone 同期処理(cli-kintone の updateKey指定による複数レコード更新)
kintoneアプリ間のレコード同期処理を cli-kintone を使い行います。
同期シナリオ
- レコードのバックアップ処理
- 定期的なバックアップ
環境
- macOS 10.13.6
- Ruby 2.5.0
- cli-kintone 0.9.4
前提条件
- 更新用のキーフィールドを用意する。(今回はフィールドコード:顧客コードを用意)
- 更新用のキーは 数値、必須、重複無し 、1から順に割り振られている。(1,2,3..,n)
- 処理は更新元のマスタアプリからバックアップ先のアプリへの一方向。
- テーブルの更新は無い。
処理概要
バックアップ先のレコードの最終更新日付を取得して、更新元のマスタアプリ(MA)のレコードの最終更新日付と比較し
バックアップ先の最終更新日付以降に更新されたマスタアプリのレコードをバックアップ先アプリ(BA)へ更新します。
- バックアップ先のレコードの最終更新日付を取得(例:更新日時→"2019-10-07T13:22:00Z")
- バックアップ先の最終更新日付以降に更新された更新元のレコードを取得
- 更新用のデータに変換
- 取得したマスタのレコードをバックアップ先に更新
全体コード
put_records_by_updatekey.rbrequire 'minitest/autorun' require 'rubygems' require 'bundler/setup' require 'dotenv/load' require 'open3' require 'csv' require 'stringio' require 'date' require 'pp' SUBDOMAIN = ENV['SUBDOMAIN'] APP_FROM = ENV['APP_FROM'] API_FROM = ENV['API_FROM'] APP_TO = ENV['APP_TO'] API_TO = ENV['API_TO'] SELECT_COLUMN = ENV['SELECT_COLUMN'] UPDATE_COLUMN = ENV['UPDATE_COLUMN'] LAST_UPDATE_COLUMN = ENV['LAST_UPDATE_COLUMN'] # 1. バックアップ先のレコードの最終更新日時を取得 def fetch_lastupdate_by_backupapp(domain, app_to, api_to, select_column) query = '"order by 更新日時 desc limit 1"' cap_str = %Q(cli-kintone -d #{domain} -a #{app_to} -c '\"#{select_column}\"' -t #{api_to} -q #{query}) out, err, status = Open3.capture3(cap_str) if (err != "") pp err; pp status; exit end if (out.size > 0) begin lines = [] out.each_line { |line| lines << (line.chomp).parse_csv } lines[1][1] rescue => e pp e exit end else return 0 end end # 2. バックアップ先の最終更新日時以降に更新された更新元のレコードを取得 def fetch_update_records_by_master(domain, app_from, api_from, select_column, datetime) query = "\"更新日時 > \\\"#{datetime}\\\" order by 更新日時 asc\"" cap_str = %Q(cli-kintone -d #{domain} -a #{app_from} -c '\"#{select_column}\"' -t #{api_from} -q #{query}) out, err, status = Open3.capture3(cap_str) puts out if (err != "") pp err; pp status; exit end out end # 3. 更新用のデータに変換 def convert_records_by_updatekey(update_column, records_string) csv = CSV.new(records_string, headers: true) csv_out = CSV.new(StringIO.new) << CSV.new(update_column).shift csv.each {|row| csv_out << row} csv_out.string end # 4. 取得したマスタのレコードをバックアップ先に更新 def update_as_backupapp(domain, app_to, api_to, records_string) cap_str = %Q(cli-kintone --import -d #{domain} -a #{app_to} -t #{api_to}) csv_out = StringIO.new csv_out << records_string out, err, status = Open3.capture3(cap_str, :stdin_data => csv_out.string) if (err != "") pp err; pp status; exit end out end # UnitTest class AddDifferenceRecordsTest < Minitest::Test def test_1 puts __method__ records = fetch_lastupdate_by_backupapp(SUBDOMAIN, APP_TO, API_TO, LAST_UPDATE_COLUMN) puts records.class io_string = StringIO.new(records) ary = io_string.readlines assert_equal 1, ary.size assert_equal DateTime.parse("2019-10-10T10:07:00Z").class, DateTime.parse(ary[0]).class end def test_2 puts __method__ records = fetch_update_records_by_master(SUBDOMAIN, APP_FROM, API_FROM, SELECT_COLUMN, "2019-10-07T13:22:00Z") sio = StringIO.new(records) ary = sio.readlines assert_equal 5, ary.size end def test_3 puts __method__ records = <<EOS "住所","担当者名","部署名","FAX","顧客コード","顧客名","備考","郵便番号","TEL","メールアドレス" "岐阜県岐阜市××××","下山 達士","情報システム部","050-××××-××××","20","金都運総研","備考欄更新","5010001","090-××××-××××","shimoyama_tatsuhito@example.com" "大阪府大阪市北区梅田××××","上野 裕太郎","経理部","050-××××-××××","14","有限会社亀山","","5300001","090-××××-××××","ueno_yuujirou@example.com" EOS str = convert_records_by_updatekey(UPDATE_COLUMN, records) sio = StringIO.new(str) header = sio.readline.chomp assert_equal UPDATE_COLUMN, header end def test_4 puts __method__ records = <<EOS "住所","担当者名","部署名","FAX","*顧客コード","顧客名","備考","郵便番号","TEL","メールアドレス" "岐阜県岐阜市××××","下山 達士","情報システム部","050-××××-××××","20","金都運総研","備考欄更新","5010001","090-××××-××××","shimoyama_tatsuhito@example.com" "大阪府大阪市北区梅田××××","上野 裕太郎","経理部","050-××××-××××","14","有限会社亀山","","5300001","090-××××-××××","ueno_yuujirou@example.com" EOS ret = update_as_backupapp(SUBDOMAIN, APP_TO, API_TO, records) assert_equal false, ret !~ /SUCCESS/ end end処理説明
主な処理を説明します。
1. バックアップ先のレコードの最終更新日時を取得
バックアップ先のアプリに対して、クエリで更新日時の降順でソートした先頭1レコードを取得します。
今回は、最新の更新日時だけ取得できれば良いので、order by 更新日時 desc
でレコードを日時の新しい順の並びで取得し、limit 1
でその先頭1レコードだけを取得しています。「order by 句 を省略した場合は、レコードIDの降順(desc)で返されます」
ちなみに、
order by 更新日時
でも良いです。デフォルトは desc なので省略ができます。query = '"order by 更新日時 desc limit 1"'2. マスタアプリから、バックアップ先の最終更新日時以降に更新されたレコードを取得
クエリ指定でマスタアプリのレコードを取得します。
日時の指定が少し読みにくいですが、クエリは下記のようになります。
"更新日時 > \"2019-10-07T13:22:00Z\" order by 更新日時 asc"
日付を囲むダブルクォーテーションをバックスラッシュでエスケイプします。
datetimeには 1.で取得したバックアップ先の最終更新日時がセットされます。
(例:"2019-10-11T10:15:00Z")query = "\"更新日時 > \\\"#{datetime}\\\" order by 更新日時 asc\""例えば、毎日決まった時刻に 「前日の更新分を定期取得する」 のなら、更新日時の条件は
FROM_TODAY
関数を使って、引数に(-1, DAYS)
(1日前の指定) を使って
"更新日時 > FROM_TODAY(-1, DAYS) order by 更新日時 asc"
でも良いかも知れません。3. 更新用のデータに変換
cli-kintone を使う最大のメリットはレコード更新にあるのでは無いかと個人的に思っています。REST API を使うと結構大変。
更新系は、レコードIDを使う方法と重複禁止フィールドをキーにする方法の2つがありますが、重複禁止フィールドをキーにする方法はcli-kintoneなら、読み込むCSVのヘッダーの項目に、アスタリスク(*)を付けるだけで行けます。
"住所","担当者名","部署名","FAX","*顧客コード","顧客名","備考","郵便番号","TEL","メールアドレス"
今回は、プログラムでアスタリスクを付けた更新用のヘッダーを用意して、2.で取得したデータのヘッダーと入れ替える処理をしています。
4. 取得したマスタのレコードをバックアップ先に更新
3.で作成した更新用のデータを
--import
を指定して標準出力から読み込みます。%Q(cli-kintone --import -d #{domain} -a #{app_to} -t #{api_to})参考リンク
cli-kintone関連
- はじめようkintone コマンドライン
- 第3回 レコードの更新をしてみよう
- レコードの一括更新
- クエリ指定
- kintone コマンドラインツールの使い方
- 定期実行でデータの同期を実現するスマートな方法 その1〜cli-kintone編〜
- https://github.com/kintone/cli-kintone
- https://developer.kintone.io/hc/en-us/articles/115002614853
- kintoneコマンドラインツールでUPSERTしてみた
Ruby関連
- 投稿日:2019-10-11T06:02:10+09:00
Controllerのコールバックメソッド(before/after_action)
目的
Ruby on Railsでアプリを使用する際にコールバックメソッドを使用するが、詳細まで理解する必要があると思ったため備忘録的に残しておく。
コールバックメソッドとは
オブジェクトの特定のタイミングで呼び出されるメソッドのこと。
その中でもよく使われるbefore/after_actionを紹介する。before_action,after_actionとは
Controllerでbefore_actionを定義することで、アクションの前後に処理(フィルター)を差し込むことが可能になります。
一般的に、複数のアクションで共通して必要になる処理などを定義することが多い。before_actionの使い方
Controllerにbefore_actionとして処理したいメソッドを定義します。
※after_actionも使い方は同一です。blogs_controller.rbclass UsersController < ApplicationController before_action :set_blog ・・・ def set_blog @blog = "before_actionの学習中" end endshow.html.erb<h1>Listing Users</h1> <p>before_actionで定義した@blog: <%= @blog.id %></p> <!— ←この行を追加 —> <table> </table> <br><img width="653" alt="スクリーンショット 2019-10-10 22.45.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509302/2ad90bd9-5974-cf20-9eff-eebef1b4de1b.png"> <%= link_to 'New User', new_blog_path %>アプリケーションを起動した画面が以下になります。
上記で定義したset_blogメソッドの@blogの値が表示されていますね。
これによりアクションの前に処理を差し込むことができました。only,excect,if,unlessオプション
railsの他のメソッド同様にオプションを持っています。
class UsersController < ApplicationController before_action :set_blog, only:[:new, edit] end上記のようにonlyオプションを使って書いた場合set_blogはnew,editアクションの前だけで実行されます。
exceptオプションは逆で指定したアクション以外でbefore_actionを実行します。また、ifオプションはラムダを渡すことによって式がtrueの時だけ実行させることができます。
unlessオプションは式がtrue以外の時だけ実行される。class UsersController < ApplicationController # current_user.editable?がtrueの時に before_action :set_blog, if: -> { current_user.editable? } endまとめ
・before/after_actionはControllerの前後に処理を差し込むことができる
・only,except,if,unlessオプションを使うことで条件付きでコールバックを使用できる
- 投稿日:2019-10-11T02:32:25+09:00
ナポレオンソート~銃口ソートから発想を得た革命ソート~
銃口ソートからソートの哲学を感じた。
元記事は、ソートのテストコードに食わせて
True
が返却されればよいというのが銃口である。銃口に感じる一因に、ソート後のオブジェクトのクラス(挙動)が変わる点もあげられる。また、銃口ソートは$O(N)$である。
$O(1)$ソートを実現したいが、もちろん粛清はしたくない。ソートの法則(銃口ソートとほぼ同じ)
i < j の場合
arr[i] <= arr[j]
かつ、
i > j の場合
arr[i] >= arr[j]
が成り立てばいいのだろう?
Rubyの世界で革命を起こす
Rubyの世界の(破壊的)ソートを$O(1)$にしてみせよう。
revolution_sort.rbclass Affected include Comparable def initialize org, revolution_number @revolution_number = revolution_number @org = org end def <=> o revolution_number <=> o.revolution_number; end def method_missing method, *args @org.__send__ method, *args end protected def revolution_number @revolution_number end end class Array def sort! # sort!をO(1)にオーバライド @revolution = true self end alias :__private_get :[] def [] i if @revolution Affected.new __private_get(i), i else __private_get i end end end a = [2,1,3] puts a[0] <= a[1] # => false puts a[0] >= a[1] # => true puts a[1] <= a[2] # => true puts a[1] >= a[2] # => false a.sort! puts a[0] <= a[1] # => true puts a[0] >= a[1] # => false puts a[1] <= a[2] # => true puts a[1] >= a[2] # => false$O(1)$のために、全てを粛清する必要はない。革命を起こせばいいのだ。
黒魔術による革命がもたらしたものは混沌謝辞
当初元記事を読み誤り記事を書きましたが、計算量とmethod missing以外は元ネタと同じです。
勝手にソート名を変えた
多分既出だと思うけどこんなもん既出かどうか調べている暇があったら寝なさい。
睡眠不足はいい仕事の大敵です。1
寝不足になりました2
余談ですが、当初
method_missing
ではなく、オープンクラスでdefine_method
を使用して、revolution_numberをインスタンス変数に持たせる方針で動作確認したところ、can't modify frozen Integer (FrozenError)
の回避策がわからず断念しました。 ↩
- 投稿日:2019-10-11T01:14:52+09:00
Webサーバのファイルを、HTTP Range Headerを使ってRuby IO classに擬態化させる
はじめに
Slackのruby-jpで、S3上のデカいzipファイルを解凍しながらダウンロードしたいと書いてる人がいて、それに対してHTTP Range Headerをいい感じにするIOを作るというアイデアを提案してる人がいたので作ってみた。
ちなみに s3io というgemがまさにそれなのだが、aws-sdkに依存している(しかも古いバージョンのまま更新されてない)ので、HTTP Range Headerを直接使ってS3以外でも使えるものを作った。
完成品
# rangeio.rb require 'net/http' require 'uri' class HTTPRangeIO def initialize(url, clazz = Net::HTTP) uri = URI.parse(url) @http = clazz.start(uri.hostname, uri.port, use_ssl:(uri.scheme == 'https')) @path = uri.request_uri @pos = 0 end def tell @pos end def seek(offset, whence = IO::SEEK_SET) case whence when IO::SEEK_CUR @pos += offset when IO::SEEK_END @pos = size + offset else @pos = offset end raise Errno::EINVAL if @pos < 0 end def size @content_length ||= @http.request_head(@path).content_length end def read(length = nil, outbuf = "") first = @pos last = length ? (@pos + length) : size return "" if first == last request = Net::HTTP::Get.new(@path) request.range = Range.new(first, last, true) response = @http.request(request) @pos += response.content_length outbuf.replace(response.body) end def close @http.finish unless @cloned end def initialize_copy(obj) @cloned = true super end def method_missing(name, *args) # do nothing end endIOと言いながら読み込みにしか対応してないのがダサいところではあるが、今回の要件では
tell
,seek
,size
,read
,close
を実装すれば動いたのでこれだけで。
initialize_copy
はdupされたインスタンスがcloseされたときにセッションをクローズしないようにするため、method_missing
はサポートされてない(主に書き込み用の)メソッドを呼ばれても落ちないようにするためにある。使い方
考え方は
StringIO
と同じで、URLをコンストラクタに渡すと、それをIOとして扱うことができるようになる。プログラム
# sample.rb require 'zip' require 'rangeio' url = "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" io = HTTPRangeIO.new(url) zf = Zip::File.new(io, false, true) zf.each do |entry| puts entry.name end zf.close io.closeここではzipライブラリとして
rubyzip
を使っているが、読み込み専用のZip::InputStream
を使ってもおそらく問題はない。
Zip::File.open_buffer
はIOを引数にとるが、zipファイルを更新するためのメソッドなので、読み込みのみでも書き込み用のメソッドが走ってしまうらしいので、使わないことにした。実行結果
$ ruby sample.rb awscli-bundle/install awscli-bundle/packages/urllib3-1.22.tar.gz awscli-bundle/packages/PyYAML-3.13.tar.gz awscli-bundle/packages/futures-3.3.0.tar.gz awscli-bundle/packages/python-dateutil-2.8.0.tar.gz awscli-bundle/packages/s3transfer-0.2.1.tar.gz awscli-bundle/packages/colorama-0.4.1.tar.gz awscli-bundle/packages/six-1.12.0.tar.gz awscli-bundle/packages/colorama-0.3.9.tar.gz awscli-bundle/packages/PyYAML-5.1.2.tar.gz awscli-bundle/packages/python-dateutil-2.6.1.tar.gz awscli-bundle/packages/rsa-3.4.2.tar.gz awscli-bundle/packages/argparse-1.2.1.tar.gz awscli-bundle/packages/simplejson-3.3.0.tar.gz awscli-bundle/packages/awscli-1.16.256.tar.gz awscli-bundle/packages/docutils-0.15.2.tar.gz awscli-bundle/packages/pyasn1-0.4.7.tar.gz awscli-bundle/packages/virtualenv-15.1.0.tar.gz awscli-bundle/packages/urllib3-1.25.6.tar.gz awscli-bundle/packages/botocore-1.12.246.tar.gz awscli-bundle/packages/jmespath-0.9.4.tar.gz awscli-bundle/packages/ordereddict-1.1.tar.gz awscli-bundle/packages/setup/setuptools_scm-1.15.7.tar.gz
- 投稿日:2019-10-11T01:14:52+09:00
Webサーバのファイルを、HTTP Range Headerを使ってRuby IOクラスに擬態化させる
はじめに
Slackのruby-jpで、S3上のデカいzipファイルを解凍しながらダウンロードしたいと書いてる人がいて、それに対してHTTP Range Headerをいい感じにするIOを作るというアイデアを提案してる人がいたので作ってみた。
ちなみに s3io というgemがまさにそれなのだが、aws-sdkに依存している(しかも古いバージョンのまま更新されてない)ので、HTTP Range Headerを直接使ってS3以外でも使えるものを作った。
完成品
rangeio.rbrequire 'net/http' require 'uri' class HTTPRangeIO def initialize(url, clazz = Net::HTTP) uri = URI.parse(url) @http = clazz.start(uri.hostname, uri.port, use_ssl:(uri.scheme == 'https')) @path = uri.request_uri @pos = 0 end def tell @pos end def seek(offset, whence = IO::SEEK_SET) case whence when IO::SEEK_CUR @pos += offset when IO::SEEK_END @pos = size + offset else @pos = offset end raise Errno::EINVAL if @pos < 0 end def size @content_length ||= @http.request_head(@path).content_length end def read(length = nil, outbuf = "") first = @pos last = length ? (@pos + length) : size return "" if first == last request = Net::HTTP::Get.new(@path) request.range = Range.new(first, last, true) response = @http.request(request) @pos += response.content_length outbuf.replace(response.body) end def close @http.finish unless @cloned end def initialize_copy(obj) @cloned = true super end def method_missing(name, *args) # do nothing end endIOと言いながら読み込みにしか対応してないのがダサいところではあるが、今回の要件では
tell
,seek
,size
,read
,close
を実装すれば動いたのでこれだけで。
initialize_copy
はdupされたインスタンスがcloseされたときにセッションをクローズしないようにするため、method_missing
はサポートされてない(主に書き込み用の)メソッドを呼ばれても落ちないようにするためにある。使い方
考え方は
StringIO
と同じで、URLをコンストラクタに渡すと、それをIOとして扱うことができるようになる。プログラム
sample.rbrequire 'zip' require 'rangeio' url = "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" io = HTTPRangeIO.new(url) zf = Zip::File.new(io, false, true) zf.each do |entry| puts entry.name end zf.close io.closeここではzipライブラリとして
rubyzip
を使っているが、読み込み専用のZip::InputStream
を使ってもおそらく問題はない。
Zip::File.open_buffer
はIOを引数にとるが、zipファイルを更新するためのメソッドなので、読み込みのみでも書き込み用のメソッドが走ってしまうらしいので、使わないことにした。実行結果
$ ruby sample.rb awscli-bundle/install awscli-bundle/packages/urllib3-1.22.tar.gz awscli-bundle/packages/PyYAML-3.13.tar.gz awscli-bundle/packages/futures-3.3.0.tar.gz awscli-bundle/packages/python-dateutil-2.8.0.tar.gz awscli-bundle/packages/s3transfer-0.2.1.tar.gz awscli-bundle/packages/colorama-0.4.1.tar.gz awscli-bundle/packages/six-1.12.0.tar.gz awscli-bundle/packages/colorama-0.3.9.tar.gz awscli-bundle/packages/PyYAML-5.1.2.tar.gz awscli-bundle/packages/python-dateutil-2.6.1.tar.gz awscli-bundle/packages/rsa-3.4.2.tar.gz awscli-bundle/packages/argparse-1.2.1.tar.gz awscli-bundle/packages/simplejson-3.3.0.tar.gz awscli-bundle/packages/awscli-1.16.256.tar.gz awscli-bundle/packages/docutils-0.15.2.tar.gz awscli-bundle/packages/pyasn1-0.4.7.tar.gz awscli-bundle/packages/virtualenv-15.1.0.tar.gz awscli-bundle/packages/urllib3-1.25.6.tar.gz awscli-bundle/packages/botocore-1.12.246.tar.gz awscli-bundle/packages/jmespath-0.9.4.tar.gz awscli-bundle/packages/ordereddict-1.1.tar.gz awscli-bundle/packages/setup/setuptools_scm-1.15.7.tar.gz