Rubyで電話番号整形

どもです。最近は夕方になっても暗くならなくなって時間間隔が微妙に狂いかかっているところですが皆さんお元気ですか?

さて、自身初のRubyGemを公開したので宣伝Postします。ステマじゃないです。ガチマです。

URL

tel_formatter | RubyGems.org | your community gem host
iTakeshi/tel_formatter · GitHub

できること

  • 電話番号の整形(市外局番は辞書ファイルから参照)
  • 全角とか余計な記号が入ってても大丈夫

これだけですが意外と既存のGemが落ちてなかったので練習問題として作ってみました。

TODO

  • ハイフン以外の区切り文字とか、(03)1234-5678みたいな出力形式に対応したい。

使い方

# Gemfile
gem 'tel_formatter'
require 'tel_formatter'

TelFormatter.format('0312345678')
# => '03-1234-5678'
TelFormatter.format('0126712345')
# => '01267-1-2345'
TelFormatter.format('03.1234.5678')
# => '03-1234-5678'

# 桁数がおかしいものや、存在しない市外局番なんかは弾く
TelFormatter.format('031234567')
# => ArgumentError

Gemの公開ってこんなに簡単だったんですね、という感じですね。 これからはもっと積極的に公開していけるといいと思いました。

Travisはすごく便利なんですが連携させたいCoverallsがいつまでたってもcoverage: unknownなのが気に食わないです。HQに連絡すれば治るという噂ですがどうなんですかね。

それではごきげんよう

「それNArrayでできるよ」をもっと便利にした - 札幌市中央区Ruby会議01

もう先週の話になってしまいましたが、Tricknotesさん主催の札幌市中央区Ruby会議01に参加してきました。 コンパクトな規模ながらも「非常に良いRubyistの活動が観測」(byしまださん)された、有意義かつ楽しい会議でした。

どの発表も聴き応えのあるすばらしいものでしたが、中でもtmaedaさんの「それNArrayでできるよ」は数値計算ライブラリのNArrayをbetter Excelとして使ってしまうコペルニクス的発想で衝撃的でした。

この発表のなかで、NArrayの軸をHuman-Readableな形式で扱えるラッパークラスを作ると便利だよ、というお話がありましたが、

これをもっと便利にするNArrayCubeクラスを試験的に実装してみたので紹介します。

使い方

# 店、世代、性別の3軸からなる売上表を作成
cube = NArrayCube.new(:int,
                      { title: :shop,       labels: ['A', 'B', 'C'] },
                      { title: :generation, labels: ['teens', 'twenties', 'thirties', 'fourties'] },
                      { title: :gender,     labels: ['men', 'women'] }
                     )

# 店A、20代、男性の売上を入力
cube.set({ shop: 'A', generation: 'teens', gender: 'men' }, 100)

# 店A、10代の売上を入力
cube.set({ shop: 'A', generation: 'teens' }, [90, 110])

# 店Bの売上を一気に入力(4世代 × 2性別のArray)
cube.set({ shop: 'B'}, [ [50, 80, 60, 75], [70, 40, 35, 50] ])

# 店Cの売上を一気に入力:第2引数を一次元のArrayで与えると勝手に4x2に変換してくれる
cube.set({ shop: 'C'}, [70, 80, 55, 60, 30, 45, 70, 60])

# 10代の売上を取得
cube.get({ generation: 'teens' })
#=>
# #<NArrayCube: ...
# [ [ 90, 50, 70 ], 
#   [ 110, 70, 30 ] ]>

# 10代の売上の合計
cube.get({ generation: 'teens' }).sum
#=> 420

# 10代の売上を店ごとに集計 => 男性+女性の値を計算すればよい
cube.get({ generation: 'teens' }).sum(:gender)
#=>
# NArray.int(3): 
# [ 200, 120, 100 ]

ツボ

  • NArrayCube#setで複数のセルに一気に値を流し込める
  • NArray#sumNArrayCube#sumでラップしたことにより、軸の指定をHuman-Readableに行える

TODO

  • spec書く
  • document書く
  • NArray#sumだけじゃなくprod/max/minとかその他のメソッドにも対応したい => method_missingとか使えるかな?
  • 「10代の売上を店ごとに集計」はcube.get({ generation: 'teens' }).sum_by(:shop)みたいに書けたほうが直感的
  • getで軸ラベルを複数指定できたほうがいいcube.get({ shop: ['A', 'B'] })
  • tmaedaさんがいいよって言ってくれたらGem化して公開

コード

require 'narray'

class NArrayCube
  NARRAY_TYPES = %i(byte sint int sfloat float scomplex complex object)

  class NArrayAxis
    def initialize(id, title, labels)
      @id = id
      @title = title.to_s
      @labels = labels.map(&:to_s)
    end

    def value_index(value)
      @labels.index(value.to_s)
    end

    attr_reader :id, :title, :labels
  end

  def initialize(type, *axes)
    if NARRAY_TYPES.include?(type)
      @type = type
    else
      raise ArgumentError
    end

    @axes = axes.map.with_index do |a, i|
      NArrayAxis.new(i, a[:title], a[:labels])
    end

    @narray = NArray.method(type).call(*@axes.map { |a| a.labels.length })
  end

  def get(conditions)
    conditions = build_condition(conditions)
    values = @narray[*conditions]

    axes = conditions.map.with_index do |c, id|
      if c.class == Array
        a = @axes[id]
        { title: a.title, labels: a.labels.select.with_index { |v, i| c.include? i } }
      else
        nil
      end
    end

    NArrayCube.new(@type, *axes.compact).set({}, values)
  end

  def set(conditions, values)
    conditions = build_condition(conditions)

    unless [Array, NArray].include?(values.class)
      @narray[*conditions] = values
    else
      partial = @narray[*conditions]
      values = NArray.to_na(values)

      if partial.shape == values.shape
        @narray[*conditions] = values
      elsif partial.total == values.total && values.shape.length == 1
        @narray[*conditions] = values.reshape(*partial.shape)
      else
        raise ArgumentError
      end
    end

    self
  end

  def sum(*titles)
    if titles.length > 0
      @narray.sum(*titles.map { |t| axis(t).id })
    else
      @narray.sum
    end
  end

  private
  def build_condition(conditions)
    cond = @axes.map { |a| (0..(a.labels.length - 1)).to_a }
    conditions.each do |title, label|
      a = axis(title)
      cond[a.id] = a.value_index(label)
    end
    cond
  end

  def axis(title)
    @axes.select { |a| a.title == title.to_s }.first
  end
end

Railsでbefore_validationコールバックを使う時の落とし穴

TL;DR: before_validationに登録されているコードがfalseを返すと、obj.savefalseを返しobj.save!は例外を投げます。DBには何も登録されません。

Railsのコールバック

便利ですよね!使ってますか?

Rails Guidesの一節を引用してみます。

Callbacks are methods that get called at certain moments of an object's life cycle. With callbacks it is possible to write code that will run whenever an Active Record object is created, saved, updated, deleted, validated, or loaded from the database.

すなわち

コールバックは(ActiveRecordの)オブジェクトのライフサイクルの中の特定の時点で呼び出されるメソッドです。コールバックを利用することで、ARオブジェクトが作られるとき・保存されるとき・更新されるとき・削除されるとき・validationが実行されるとき・DBから読み込まれるとき などに必ず実行されるコードを登録することができます。

controllerになんでもかんでも任せてしまうのを防ぎ、modelの責務をmodelに記述することができるようなるのでとても有用です。

※上記guidesやreferencesと合わせて、昨年末のRoR Advent Calendarに投稿されたこの記事を読むと勉強になります ↓
Ruby - てめえらのRailsはオブジェクト指向じゃねえ!まずはCallbackクラス、Validatorクラスを活用しろ! - Qiita [キータ]

before_validation

ここから本題です。

例題として、RailsSNSを構築することを考えます。ユーザーはメールアドレスに紐付けてアカウントを取得し、登録するとメールアドレス確認メールが送られてきます。メールアドレスの確認が完了しないと、SNS上で活動し始めることができません。利用開始後にユーザーがメールアドレスを変更した場合も、確認のメールが送られてきます。

# db/migrate/20140104120000_create_users.rb

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :name
      t.string :email
      # some more columns...

      t.string :email_verification_code # メールアドレス確認用乱数
      t.boolean :email_verificated # メールアドレス確認済み権限フラグ
    end
  end
end

このようにテーブルを定義し、以下のようなコードを書きます。

# app/models/users.rb

class User < ActiveRecord::Base
  # validation前に、emailが変更されていればコールバック関数を呼び出す
  before_validation :set_email_verification_code, if: 'self.email_changed?'

  # 保存後、メールアドレス確認メールを送信する
  after_save :send_verification_mail, if: 'self.email_changed?'

  private
  def set_email_verification_code
    # 新しい確認用コードを生成
    self.email_verification_code = SecureRandom.hex(10)
    # 状態を「未確認」にする
    self.email_verificated = false
  end

  def send_verification_mail
    # 略
  end
end

一見、とても自然ですよね?しかしこれでは、いくらすばらしいSNSを作ってもユーザーは集まりません。なんたって、登録できないのですから!

Reference中の一文を引用します。

If the returning value of a before_validation callback can be evaluated to false, the process will be aborted and Base#save will return false. If ActiveRecord::Validations#save! is called it will raise a ActiveRecord::RecordInvalid exception. Nothing will be appended to the errors object.

意訳しますと

before_validationコールバックのコードがfalse(またはfalseと評価されるもの)を返すと、そこで処理が中止されます(validationは実行されません)。user.saveの途中でこれが起こると、user.saveは単にfalseを返します(DBには保存されません)。user.save!の途中でこれが起こると、ActiveRecord::RecordInvalidが起こります。いずれにしても、user.errorsにはいかなるvalidation errorも登録されません

なんということでしょう!見かけ上はvalidationに失敗しているのとまったく変わらないのに、実はvalidationそのものがスキップされているのです。

上で作ったUserモデルの例では、set_email_verification_codeの最後の式でfalseを代入しているために、メソッドそのものの戻り値がfalseになります。(Rubyでは代入式の戻り値は代入された値ですね。)

これを避けるには単にこのメソッド中の式の順番を入れ替えれば良いだけです。

  def set_email_verification_code
    # 状態を「未確認」にする
    self.email_verificated = false
    # 新しい確認用コードを生成
    self.email_verification_code = SecureRandom.hex(10) # => 戻り値が乱数文字列になる
  end

Railsチームがどのような意図でこの挙動を設計したのかわかりませんが、陥りやすい落とし穴でなおかつ一度ハマると気づいて抜け出すのが困難です(数十分悩みました)。お気をつけください。


半年ほど放置しておりましたがあけましておめでとうございます(笑
今年もマイペースに更新してまいりますので何卒よろしくおねがいします

Vim の mercurial レポジトリの HEAD が ver7.4a になってた

およそ2ヶ月ほど更新が途切れておりましたが生きています。お久しぶりです。

Vimを最強のPython開発環境にする2 - Λlisue's blog この記事を読んで、一念発起、Vimの環境を徹底的に整備しました(Λlisueさんありがとうございます!)。その中で、NeoComplcacheに代わる新しい補完PluginとしてNeoCompleteが紹介されていました。Vim 7.3.885以上かつLuaインタプリタ装備の環境下で使用でき、NeoComplcacheよりも高速に動作するということで、入れてみようとしたのですが、UbuntuのパッケージになっているVimは7.3.5xxだったのでVimをコンパイルする必要がありました。

続きを読む

Ubuntu 13.04 で音声のデジタル出力

夜ふかししてます。こんばんは。

先日、Ubuntu 13.04をインストールしたところ、音声のデジタル出力(S/PDIF)ができなくなったということを記事に書きましたが、簡単な設定変更で治りました!

ので、同じ道を通る方のために書き残しておきます。

続きを読む

Ubuntu 13.04をインストールしてみた

4/25にリリースされたUbuntu 13.04。連休を利用してインストールしてみました。Ubuntuはアップデートの時に不具合が出るというのをよく聞くので、クリーンインストールしています。

ま、Ubuntuのリリースサイクルである半年に一回くらい、ゼロベースでやり直した方が中身がゴチャゴチャしなくて好ましいという考え方もできますね。

続きを読む

Ubuntu with Intel Graphics

Ubuntu導入当初からでしたが、起動して数分内にこの画面が出ます。
f:id:iTakeshi:20130503222841j:plain
システムプログラムの問題が見つかりました」で、何が悪いの?というと、毎度毎度plymouthd(グラフィカルブートができるようにするやつ)がクラッシュなさっているそうな。

特段の実害がなかったので放置してありましたが、最近どうもChromeでサーフィンしているとカクつくなぁ…ということで、やっぱりグラフィックス周りになんか病巣があるんじゃないかと疑い、いろいろ調べてみたところ…

Intel HD Graphicsドライバ、入ってないんじゃね?

という結論に至りました。えらいこっちゃ。

追記
Ubuntu 13.04でこの問題は修正されておりますので、この記事の適用は12.10よりも古いバージョンに限定されます。

続きを読む