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

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.xmlTitlesOfParts 要素に、 シート名!印刷範囲 の形で記録されます。

<?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

ホントはプルリク出したい。

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

Railsチュートリアル 第9章<復習>

第9章の復習メモです。
個人的に重要と思ったことを書きます。

前回と同様、以下3つの視点で書きます。

  • 分かったこと
  • 分からなかったこと
  • 今回はスルーしたこと

分かったこと

永続セッション

前章では、一時cookieを使ったので、ブラウザを閉じると情報が消えてしまった。本章では、ブラウザを閉じても消えない永続cookieを使った。

前章の通りRailsでは、sessionメソッドを使うことで、一時cookieに情報を保存できる。sessionメソッドは、保存する際に情報を暗号化してくれるので、安全性が高い。
一方で、永続cookieの場合は、cookiesメソッドを使う。cookiesメソッドは、自動で暗号化する機能が無いため、安全性が低い。

今回、永続cookieのセキュリティを強化するため、以下の対策を行った。

  • cookieにユーザIDを保存する際、暗号化の処理を行う。
  • ユーザIDに加えて、サーバ上でトークンを発行し、cookieに保存する。

今回、永続セッションを作成した手順は以下の通り。

  1. ランダムな文字列を生成し、これをトークンとする。
  2. ブラウザのcookiesにトークンを保存する。有効期限も設定しておく。
  3. トークンをデータベースに保存する。その際、ハッシュ値に変換する。
  4. ブラウザのcookiesに、暗号化したユーザーIDを保存する。
  5. 永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、トークンのcookiesがデータベース内のハッシュ値と一致することを確認する。

分からなかったこと、今回はスルーしたこと

  • 永続セッション実装の詳細

今回は、セッションそのものの仕組みについて、理解することに努めました。コーディングは一通り実装してみて、流れは分かったかな、という感じです。

  • テスト全般

参考

以下を参考にさせていただきました。
https://qiita.com/hot_study_man/items/147f8b767b4135fe6fe4
https://www.masalog.site/entry/2017/08/31/171712

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

Ruby ユーザ の 入力数値 を 変数 に 格納する

目的

  • Rubyにてユーザからの入力数値を受け取り、変数に格納する方法をまとめる。

書き方の例

  • 入力を受け取るにはメソッドgetsを使用する。
  • 入力の改行もなくして変数に格納したいためオプションchompを付ける。
  • 入力が数値であるため、さらにオプションto_iを付ける。
  • 下記に処理を記載する。
変数 = gets.chomp.to_i

より具体的な例

  • 入力された数値をコンソールに出力する処理を考える。
  • コンソールには「好きな数値を入力してください。」 → 入力待ち → 「入力された数値は○○です。」と表示されるようにする。
  • 入力された数値は変数inputに格納する。
  • 下記に処理を記載する。
puts "好きな数値を入力してください。"
input = gets.chomp.to_i
puts "入力された数値は#{input}です。"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

記事投稿のテストです。

Markdownのテストです。:grinning::beginner:

見出し h1

見出し h2

見出し h3

見出し h4

見出し h5
見出し h6
  • リスト
  • リスト
    • リスト
  1. 番号付きリスト
  2. 番号付きリスト
    1. 番号付きリスト
  • チェックボックス
  • チェックボックス

code

Left align Right align Center align
This This This
column column column
will will will
be be be
left right center
aligned aligned aligned

alt

qiita.rb
puts 'code with syntax'

IMG_8360.jpg

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

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度目のデプロイですが、やはり一筋縄ではいきません。ハマったエラーはメモって次回はハマらないようにしていきます。

終わり

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

【初心者向け】PG二年目の私がコーディングでもっと早くから気をつけたかったこと

はじめに

コードを書く時、「こういう時ってどう書いたらいいの?」「そもそも何が綺麗なコードなの?」と悩んだことはありませんか?
私はあります。
この記事はかの有名な リーダブルコード を読んだ際、私自身が出来ていなかったこと、意識しなければならないと思ったことを纏めたものです。
自分なりに纏めていますので、おかしい記述等もあるかもしれませんが、その際はご指摘ください。

命名規則について

具体的な名前を付ける

  • 誰が見ても、その名前だけで以下の2点が分かるような名前を付ける。
    • 何をするメソッドか
    • 何が格納されている変数か
  • 以下の項目を意識すると具体的な名前になりやすい。
    • 動詞を使うときは、より明確な動詞を選ぶ。汎用的なものはなるべく使わない。(類語を調べ、その中から選ぶといい)
      • 私はこちらの記事を参考にするようにしています。
    • 値の単位を付ける。(時間、サイズなど)
    • 「どんな」を意識する。具体的にいうと形容詞や中身を連想できる名詞を添える。
      • records よりも messages、 ハッシュを格納する変数であれば○○_hash の方が直感的に中身の想像が出来る。

リファクタリングするときに意識すること

ベースとして意識することについて

  • 同じことは何度も書かない。
    • 同じことを何度も書く必要がある場合はメソッドに分ける、変数に格納する
  • 並び順に意味を持たせる。
    • 複数変数が出てくる場合、定数が複数出てくる場合はその並びに意味を持たせる。
    • 例:アルファベット順、画面での並び順、ファイルへの出力順など
  • 行数が多い場合は適度に改行を挟み、見やすくする。
    • 例:処理のまとまり単位で改行、など。

コメントについて

書いてはいけないコメント

  • コードを見れば誰でも分かること
  • メソッド名や変数名について説明するようなコメント(それは付けている名前が悪い)

不具合があるコードに付けるコメント

  • 以下のprefixを付ける
prefix 意味
TODO: あとで直す
FIXME: 既知のバグがあるコード
HACK: あまり綺麗じゃない
XXX: 大きな問題がある

prefixが大文字の場合は大きな問題、小文字の場合は小さな問題という意味合いになります。
TODO(ラテ太郎): みたいな書き方をします。

コメントを書いてもいい場所

  • 読み手が一見で分からない or 疑問を持ちそうなところ
    • なんで?これは何?と思われるようなコードであるならば、コメントの記載が必要です。
  • 間違えて使われる可能性がありそうなところ

簡潔なコメントの書き方

  • そののような指示語は避ける。(自分が思っているものと違う認識をされる可能性がある為、具体的に書くのが良いです。)
  • かどうかという表現は使わない。
    • どうなったらどう、という書き方を心がける
    • ×: Aかどうか ○ : AならばB
  • 情報密度の高い言葉を使う。
    • 自分の伝えたい内容が集約された言葉を選ぶことを意識すればコメントが簡潔になります。
  • 実例を書く

ループ・ロジックの単純化

条件式

  • 条件は肯定が正義。(わかりやすい)
    • ド・モルガンの法則を駆使してなるべく肯定的な条件になるようにする。否定の否定、は使わない。悪。
  • 目立つ条件、重要な条件から書く。
  • 何でもかんでも三項演算子を使わない(わかりにくくなるため)
  • 早期リターンできる時は早期リターンする(早期リターンすることでネストが減る。)

まとめ

やっぱり命名って難しい!!!!!
まずは上記のような、常に付きまとってくる問題と戦いながら、可読性の高いコードを書けるようになりたいと思います。

参考

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

データを削除しようとしたらそのテーブルに主キーがなくて消せないんだけど

冒頭

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つよつよの方へ

もしなにかしら方法をしっていたら教えてください?‍♂️
いや、普通に主キー作ればいいんだけどさ...

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

【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

参考

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

「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 or select 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はあまり使ったことがなかったので、使える場面では使っていきたいと思います。
また、自分の書くコードは冗長になりがちなので、「これって冗長じゃない?」という気持ちをもってコーディングすることを意識する必要があるなと感じました。
次の回ではもっといいコードを書けるように腕を磨いておきます。

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

Rubyのヒアドキュメントまとめ

Rubyを勉強していて、ヒアドキュメントなる便利な記法を学んだので
備忘録を兼ねてまとめてみます。

前提条件

この記事ではRubyのバージョンとして2.4を前提としています。
そのため、それ以外のバージョンでは動きが異なる可能性がありますので
注意してください。

ヒアドキュメントとは

ヒアドキュメントは複数行に渡る文字列を入力する際に非常に便利な記法です。
記述する際に使う必要最低限の文字としては
>>,任意の文字列(ヒアドキュメントの中に記述する文字列と重複しないもの)
の2つとなります。

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

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#destroy

flash.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/063957

sessionメソッドの使い方

session情報の追加

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id  # ← session情報にユーザIDを追加
  end
end

session情報の削除

deleteメソッドを使う。

session.delete(:user_id)

分からなかったこと、今回はスルーしたこと

  • テスト全般
  • アプリケーションの仕様の詳細
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[初学者]rootメソッド

目的

学習の備忘録と初学者の参考資料として投稿

ルートの設定

ルートへアクセスした場合に特定のアクションを実行させたい時に設定。

設定方法

config/routes.rb
 root to: コントローラー名#アクション名

ブラウザで

 http://localhost:3000/と記入

結果、設定したアクションのviewが表示されます。

まとめ

最初に表示したいトップページがあるならば、ルート設定すればいい。

今後も学習で気づきや参考になるものがあれば、アップしていきます。
もし参考になったらいいね!!よろしくお願いします:bow_tone1:

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

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使えるようになる

参考

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

[初学者]ルーティングについて

目的

学習の備忘録と初学者の参考資料として投稿

ルーティング

ルーティングは、ブラウザから届いたリクエスト(HTTPメソッド+URL)に対して、コントローラーで定義したアクションを結びつけるルールです。
884f611b6e8d4a5af88d3211daa4bc1b.png

上記は参考例です。

ルーティングの確認

ターミナルで

$ rails routes

あるいは...ブラウザで

http://localhost:3000/rails/info/routes と入力

どちらでも確認出来ます。

HTTPメソッド

HTTPメソッドとは、「クライアントがサーバーにしてほしいことを依頼するための手段」のこと。
主に使うのは『GET』『POST』『PUT』『DELETE』の4つぐらいです。
それぞれの働きは

『GET』  ・・・データを取得するときに利用する。

『POST』  ・・・サーバーにデータを送信する時に利用する。アカウント作成や投稿するなど新規作成で使われる。

『PUT』  ・・・サーバーにデータを送信する時に利用する。既存データの更新などで使われる。

『DELETE』・・・既存データを削除するときに利用する。

任意のアクションを呼び出したい時は

 http://(ホスト名)/コントロール名/アクション名

で呼び出すことが可能です。

まとめ

今回は簡単なさわりだけを書いています。今後さらに深掘りして書いていきます。

今後も学習で気づきや参考になるものがあれば、アップしていきます。
もし参考になったらいいね!!よろしくお願いします:bow_tone1:

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

【Ruby】世界のナベアツのあのギャグで遊んでみよう

目次

  • 1. この記事の狙い
  • 2. ターゲット層
  • 3. 実行環境
  • 4. 世界のナベアツについて
  • 5. Rubyコード
    • 5-1. 期待する挙動
    • 5-2. Comedianクラス実装
    • 5-3. 実際に動かしてみよう

1. この記事の狙い

世の中には伊藤淳一さんチェリー本(私も大変おせわになりました:bow: )をはじめとした入門書、オンライン上でも無料の教材が揃っているので、インプットで不便することはない。
しかし、アウトプットをしないと自分の力に変換出来ないし忘れてしまう。

なるべくなら肩の力を抜いて遊びながら基礎をおさらい出来たらあまり疲れないで済むで良い。
現実世界にはそんな遊びに適した材料が転がってるので、自分のリフレッシュも兼ねて思いつく限りで記事に書き起こしてみようというのが狙い。

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="吉本興業">

違う芸人ではギャグは発動しない。

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

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 : 数字にマッチ

  • \ddは数字を表す。数字と表す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}/)
=> nil

i : 大文字・小文字を区別しない検索

  • iオプションを加えることで大文字・小文字を区別しないで検索する。
  • iオプションをつけずに[a-z]と小文字で記述すると大文字にマッチしなくなる。
  • 大文字・小文字の区別
ターミナル
irb(main):003:0> 'Cat'.match(/cat/)
=> nil

irb(main):004:0> 'Cat'.match(/cat/i)
=> #<MatchData "Cat">

実践的な使用例

irb
pass = '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 回以上の繰り返しにマッチ

まだまだ奥深い正規表現ですが以上のことだけは最低限おさえて置きたいと思います。。。

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

破壊的メソッド 非破壊的メソッド

  • 大元も値を変更するかいなか
破壊的メソッドとは本体のデータを変更する事

- 非破壊的

string = "test code"
string.slice!(0,4)
p string

 "test code"

- 破壊的

string = "test code"
string.slice!(0,4)
p string
 " code"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで既存モデルをポリモーフィック化したら大変だった話

前提

社内でやろうとしてたことそのまま書けないので、
例えを探しましたが・・・わかりにくいかもですw
ポリモーフィックについては、こちらの記事がわかりやすかったです!感謝感激。

モデルは下記とします。
モデル図 *()の中がモデル名
普通自動車(Car)-<部品(Part)>-人(Owner)

Car
has_many :parts
has_many :owners, through: :parts

Part
belongs_to :cars
belongs_to :owners

Owner
has_many :parts
has_many :cars, through: :parts

目的は、
「ある人が所持している、ある普通自動車に搭載されている部品達をDBで管理できる」
とします。

もうちょっと具体的にいうと、
「AさんのIDから次郎号という自動車を特定できて、
    かつ次郎号に搭載されている部品(前輪・後輪とか)も把握できる」

みたいな感じです。

異なるモデルも同一のモデルで管理したい

上記モデルをポリモーフィック可したらどうなるかというと
モデルはこんな感じになります。*()の中がモデル名
モデル図
なんかの乗り物-<部品(Part)>-人(Owner)

なんか乗り物(ここではCarとShipとしておきます)
has_many :parts
has_many :owners, as: :vehicle, through: :parts

Part
belongs_to :vehicles, polymorphic: true
belongs_to :owners

Owner
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モデルが入ろうが吸収してくれます。

参考

【初心者向け】Railsのポリモーフィック関連付けを理解しよう

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

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 モデルを作る

titlepublished の2つの属性を持つ Book モデルを作ります。

$ bin/rails g model Book title published:boolean

seed データを作る

seed データを作ります。

db/seeds.rb
Book.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

参考情報

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

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)へ更新します。

  1. バックアップ先のレコードの最終更新日付を取得(例:更新日時→"2019-10-07T13:22:00Z")
  2. バックアップ先の最終更新日付以降に更新された更新元のレコードを取得
  3. 更新用のデータに変換
  4. 取得したマスタのレコードをバックアップ先に更新

全体コード

put_records_by_updatekey.rb
require '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関連

Ruby関連

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

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.rb
class UsersController < ApplicationController
  before_action :set_blog
  ・・・
  def set_blog
    @blog = "before_actionの学習中"
  end
end
show.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の値が表示されていますね。
これによりアクションの前に処理を差し込むことができました。

スクリーンショット 2019-10-10 23.04.29.png

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オプションを使うことで条件付きでコールバックを使用できる

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

ナポレオンソート~銃口ソートから発想を得た革命ソート~

銃口ソートからソートの哲学を感じた。

元記事は、ソートのテストコードに食わせてTrueが返却されればよいというのが銃口である。銃口に感じる一因に、ソート後のオブジェクトのクラス(挙動)が変わる点もあげられる。

また、銃口ソートは$O(N)$である。
$O(1)$ソートを実現したいが、もちろん粛清はしたくない。

ソートの法則(銃口ソートとほぼ同じ)

i < j の場合

arr[i] <= arr[j]

かつ、

i > j の場合

arr[i] >= arr[j]

が成り立てばいいのだろう?

Rubyの世界で革命を起こす

Rubyの世界の(破壊的)ソートを$O(1)$にしてみせよう。

revolution_sort.rb
class 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:neutral_face:


  1. ソートってなんだよ(哲学) 

  2. 余談ですが、当初method_missingではなく、オープンクラスでdefine_methodを使用して、revolution_numberをインスタンス変数に持たせる方針で動作確認したところ、can't modify frozen Integer (FrozenError)の回避策がわからず断念しました。 

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

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

end

IOと言いながら読み込みにしか対応してないのがダサいところではあるが、今回の要件では 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webサーバのファイルを、HTTP Range Headerを使ってRuby IOクラスに擬態化させる

はじめに

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

end

IOと言いながら読み込みにしか対応してないのがダサいところではあるが、今回の要件では 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む