バリケンのRuby日記 RSSフィード

2006-07-13

[] ちょっとお休み  ちょっとお休み - バリケンのRuby日記 を含むブックマーク はてなブックマーク -  ちょっとお休み - バリケンのRuby日記  ちょっとお休み - バリケンのRuby日記 のブックマークコメント

なんかバリケン中の人が38度の熱を出して寝込んでいるみたいだから、Ruby日記はちょっとお休みみたいだよ。

トラックバック - http://rubyist.g.hatena.ne.jp/muscovyduck/20060713

2006-07-12

[] 勉強日記の効用  勉強日記の効用 - バリケンのRuby日記 を含むブックマーク はてなブックマーク -  勉強日記の効用 - バリケンのRuby日記  勉強日記の効用 - バリケンのRuby日記 のブックマークコメント

こんな記事を見つけたよ!

第2回 3年生のゼミを院生レベルに高めたブログの「外化」効果 | 認知科学者の視点 | ティータイム | wisdom Business Leaders Square

自分の考えを文章にすることを「外化」っていうみたいだよ。そして、これが学習の向上にすごく役に立つんだって。

さあ、みんなも勉強日記チャレンジしてみよう!

トラックバック - http://rubyist.g.hatena.ne.jp/muscovyduck/20060712

2006-07-11

[] QuickMLソースコードを読む(28) quickml/mail.rbの続き  QuickMLのソースコードを読む(28) quickml/mail.rbの続き - バリケンのRuby日記 を含むブックマーク はてなブックマーク -  QuickMLのソースコードを読む(28) quickml/mail.rbの続き - バリケンのRuby日記  QuickMLのソースコードを読む(28) quickml/mail.rbの続き - バリケンのRuby日記 のブックマークコメント

昨日の日記の続きだよ。といってもあんまり進んでないよ。

require 'test/unit'
require 'quickml/mail'

class TC_Mail < Test::Unit::TestCase
  include QuickML

  def setup
    @mail = Mail.new
    @mail.instance_eval{@recipients << 'user1@example.net' << 'user2@example.net'}
    @mail.instance_eval{@charset = 'iso-2022-jp'}
    @mail.instance_eval{@content_type = 'text/plain'}
  end

  def test_recipients
    assert_equal @mail.recipients[0], 'user1@example.net'
    assert_equal @mail.recipients[1], 'user2@example.net'
  end

  def test_charset
    assert_equal @mail.charset, 'iso-2022-jp'
  end

  def test_content_type
    assert_equal @mail.content_type, 'text/plain'
  end

  def test_normailze_address
    assert_equal @mail.instance_eval{normalize_address 'foo@QuickML.CoM'}, 'foo@quickml.com'

  end
end

テスト結果は次のとおりだよ。

$ ruby mail_test.rb
Loaded suite mail_test
Started
....
Finished in 0.069053 seconds.

4 tests, 5 assertions, 0 failures, 0 errors
$

全然進んでないから、ちゃんとやらないとね。

トラックバック - http://rubyist.g.hatena.ne.jp/muscovyduck/20060711

2006-07-10

[] QuickMLソースコードを読む(27) quickml/mail.rbの続き  QuickMLのソースコードを読む(27) quickml/mail.rbの続き - バリケンのRuby日記 を含むブックマーク はてなブックマーク -  QuickMLのソースコードを読む(27) quickml/mail.rbの続き - バリケンのRuby日記  QuickMLのソースコードを読む(27) quickml/mail.rbの続き - バリケンのRuby日記 のブックマークコメント

昨日の日記d:id:walf443さんからコメントをいただきました。ありがとうございます!

require 'test/unit'
require 'quickml/mail'

class TC_Mail < Test::Unit::TestCase
  include QuickML

  def setup
    @mail = Mail.new
  end

  def test_normailze_address
    assert_equal @mail.send(:normalize_address, 'foo@QuickML.CoM'), 'foo@quickml.com'
    assert_equal @mail.instance_eval{normalize_address 'foo@QuickML.CoM'}, 'foo@quickml.com'
  end
end

こう書けば、

$ ruby mail_test.rb
Loaded suite mail_test
Started
.
Finished in 0.039875 seconds.

1 tests, 2 assertions, 0 failures, 0 errors
$

おお、素晴らしい!これでプライベートメソッドも、どんどんテストが書けるね。

でもObject#sendプライベートメソッドを実行するテクニックは将来的にはサポートされないみたいだから、Object#instance_evalのほうを使うことにするよ。

トラックバック - http://rubyist.g.hatena.ne.jp/muscovyduck/20060710

2006-07-09

[] QuickMLソースコードを読む(26) quickml/mail.rbの続き  QuickMLのソースコードを読む(26) quickml/mail.rbの続き - バリケンのRuby日記 を含むブックマーク はてなブックマーク -  QuickMLのソースコードを読む(26) quickml/mail.rbの続き - バリケンのRuby日記  QuickMLのソースコードを読む(26) quickml/mail.rbの続き - バリケンのRuby日記 のブックマークコメント

昨日の日記の続きだよ。

といっても、今日は進捗なし。プライベートメソッドのテストってどうやったらいいのかな?

require 'test/unit'
require 'quickml/mail'

class TC_Mail < Test::Unit::TestCase
  include QuickML

  def setup
    @mail = Mail.new
  end

  def test_normailze_address
    assert_equal @mail.normalize_address('foo@QuickML.CoM'), 'foo@quickml.com'
  end
end

こう書いても、

$ ruby mail_test.rb
Loaded suite mail_test
Started
E
Finished in 0.043422 seconds.

  1) Error:
test_normailze_address(TC_Mail):
NoMethodError: private method `normalize_address' called for #<QuickML::Mail:0xb7c247a4>
    mail_test.rb:23:in `test_normailze_address'

1 tests, 0 assertions, 0 failures, 1 errors
$

となってエラーになっちゃうよ。evalとかを使えばできそうだけど、もっとスマートな方法ってないのかな?

追記:解決しました

walf443walf4432006/07/10 07:561.9までならObject#__send__が使えたようです。それ以降はinstance_evalを使う必要があるそうです。

トラックバック - http://rubyist.g.hatena.ne.jp/muscovyduck/20060709

2006-07-08

[] QuickMLソースコードを読む(25) quickml/mail.rb  QuickMLのソースコードを読む(25) quickml/mail.rb - バリケンのRuby日記 を含むブックマーク はてなブックマーク -  QuickMLのソースコードを読む(25) quickml/mail.rb - バリケンのRuby日記  QuickMLのソースコードを読む(25) quickml/mail.rb - バリケンのRuby日記 のブックマークコメント

今日からはquickml/mail.rbを読んでいくよ。

#
# quickml/mail - a part of quickml server
#
# Copyright (C) 2002-2004 Satoru Takabayashi <satoru@namazu.org> 
#     All rights reserved.
#     This is free software with ABSOLUTELY NO WARRANTY.
#
# You can redistribute it and/or modify it under the terms of 
# the GNU General Public License version 2.
#

require 'kconv'

module QuickML
  class MailSender
    def initialize (smtp_host, smtp_port, use_xverp = false)
      @smtp_port = smtp_port
      @smtp_host = smtp_host
      @use_xverp = use_xverp
      @xverp_available = false
    end

    def send (message, mail_from, recipients)
      recipients = [recipients] if recipients.kind_of?(String)
      s = TCPSocket.open(@smtp_host, @smtp_port) 
      send_command(s, nil, 220)
      send_command(s, "EHLO #{Socket.gethostname}", 250)
      if @use_xverp and @xverp_available and (not mail_from.empty?)
        send_command(s, "MAIL FROM: <#{mail_from}> XVERP===", 250)
      else
        send_command(s, "MAIL FROM: <#{mail_from}>", 250)
      end
      recipients.each {|recipient|
        send_command(s, "RCPT TO: <#{recipient}>", 250)
      }
      send_command(s, "DATA", 354)
      message.each_line {|line|
        line.sub!(/\r?\n/, '')
        line.sub!(/^\./, "..")
        line << "\r\n"
        s.print(line)
      }
      send_command(s, ".", 250)
      send_command(s, "QUIT", 221)
      s.close
    end

    private
    def send_command (s, command, code)
      s.print(command + "\r\n") if command
      begin
        line = s.gets
        @xverp_available = true if /^250-XVERP/.match(line)
      end while line[3] == ?-

      return_code = line[0,3].to_i
      if return_code == code
        line
      else
        raise "smtp-error: #{command} => #{line}"
      end
    end
  end

  class Mail
    def initialize
      @mail_from = nil
      @recipients = []
      @header = []
      @body = ""
      @charset = nil
      @content_type = nil
      @bare = nil
    end
    attr_reader :recipients
    attr_reader :charset
    attr_reader :content_type
    attr_accessor :mail_from
    attr_accessor :body
    attr_accessor :bare

    private
    def get_content_type
      if %r!([-\w]+/[-\w]+)! =~ self["Content-Type"]
	$1.downcase
      else
	nil
      end
    end

    def get_charset
      if /charset=("?)([-\w]+)\1/ =~ self["Content-Type"]
	$2.downcase
      else
	nil
      end
    end

    def remove_comment_in_field (field)
      field = field.toeuc
      true while field.sub!(/\([^()]*?\)/, "")
      field
    end

    # foo@QuickML.CoM => foo@quickml.com
    # "foo"@example.com => foo@example.com
    def normalize_address (address)
      name, domain = address.split('@')
      name.gsub!(/^"(.*)"$/, '\1')
      if domain
	name + "@" + domain.downcase
      else
	address
      end
    end

    def collect_address (field)
      address_regex = 
	/(("?)[-0-9a-zA-Z_.+?\/]+\2@[-0-9a-zA-Z]+\.[-0-9a-zA-Z.]+)/ #/
      addresses = []
      parts = remove_comment_in_field(field).split(',')
      parts.each {|part|
	if (/<(.*?)>/ =~ part) or (address_regex =~ part)
	  addresses.push(normalize_address($1))
	end
      }
      addresses.uniq
    end

    public
    def to_s
      str = ""
      each_field {|key, value| str << sprintf("%s: %s\n", key, value) }
      str << "\n"
      str << @body
      str
    end

    def parts
      parts = @body.split(/^--#{Regexp.escape(self.boundary)}\n/)
      parts.shift  # Remove the first empty string.
      parts
    end

    def add_recipient (address)
      @recipients.push(normalize_address(address))
    end

    def clear_recipients
      @recipients = []
    end

    def [] (key)
      field = @header.find {|field| key.downcase == field.first.downcase}
      if field then field.last else "" end
    end

    def unshift_field (key, value)
      field = [key, value]  # Use Array for preserving order of the header
      @header.unshift(field)
    end

    def push_field (key, value)
      field = [key, value]  # Use Array for preserving order of the header
      @header.push(field)
    end

    def concat_field (value)
      lastfield = @header.last
      @header.pop
      push_field(lastfield.first, lastfield.last + "\n" + value)
    end

    def each_field
      @header.each {|field|
	yield(field.first, field.last)
      }
    end

    def looping?
      !self["X-QuickML"].empty?
    end

    def from
      address = if not self["From"].empty?
		  collect_address(self["From"]).first
		else
		  @mail_from
		end
      address = "unknown" if address.nil? or address.empty?
      normalize_address(address)
    end

    def collect_cc
      if self["Cc"]
	collect_address(self["Cc"])
      else
	[]
      end
    end

    def collect_to
      if self["To"]
	collect_address(self["To"])
      else
	[]
      end
    end

    def valid?
      (not @recipients.empty?) and @mail_from
    end

    def empty_body?
      return false if @body.length > 100
      /\A[\s ]*\Z/ =~ @body.toeuc # including Japanese zenkaku-space.
    end

    def multipart?
      %r!^multipart/mixed;\s*boundary=("?)(.*)\1!i =~ self["Content-Type"] #"
    end

    def boundary
      if %r!^multipart/mixed;\s*boundary=("?)(.*)\1!i =~ self["Content-Type"]#"
	$2
      else
	nil
      end
    end


    def read (string)
      header, body = string.split(/\n\n/, 2)
      attr = nil
      header.split("\n").each {|line|
	line.xchomp!
	if /^(\S+):\s*(.*)/=~ line
	  attr = $1
	  push_field(attr, $2)
	elsif attr
	  concat_field(line)
	end
      }
      @bare = string
      @charset = get_charset
      @content_type = get_content_type
      @body = (body or "")
    end

    class << self
      def send_mail (smtp_host, smtp_port, logger, optional = {})
	mail_from = optional[:mail_from]
	recipients = optional[:recipients]
	header = optional[:header]
	body = optional[:body]
        if optional[:recipient]
          raise unless optional[:recipient].kind_of?(String)
          recipients = [optional[:recipient]] 
        end
	raise if mail_from.nil? or recipients.nil? or 
	  body.nil? or header.nil?

	contents = ""
	header.each {|field|
	  key = field.first; value = field.last
	  contents << "#{key}: #{value}\n" if key.kind_of?(String)
	}
	contents << "\n"
	contents << body
	begin
          sender = MailSender.new(smtp_host, smtp_port, true)
          sender.send(contents, mail_from, recipients)
	rescue => e
	  logger.log "Error: Unable to send mail: #{e.class}: #{e.message}"
	end
      end

      def address_of_domain? (address, domain)
	domainpat = Regexp.new('[.@]' + Regexp.quote(domain) + '$',  #'
			       Regexp::IGNORECASE)
	if domainpat =~ address then true else false end
      end

      def encode_field (field)
	field.toeuc.gsub(/[ -瑤]\S*\s*/) {|x|
	  x.scan(/.{1,10}/).map {|y|
	    "=?iso-2022-jp?B?" + y.tojis.to_a.pack('m').chomp + "?="
	  }.join("\n ")
	}
      end

      def decode_subject (subject)
	NKF.nkf("-e", subject.gsub(/\n\s*/, " "))
      end

      def clean_subject (subject)
	subject = Mail.decode_subject(subject)
	subject.gsub!(/(?:\[[^\]]+:\d+\])/, "")
	subject.sub!(/(?:Re:\s*)+/i, "Re: ")
	return subject
      end

      def rewrite_subject (subject, name, count)
	subject = Mail.clean_subject(subject)
	subject = "[#{name}:#{count}] " + subject
	Mail.encode_field(subject)
      end

      def join_parts (parts, boundary)
	body = ""
	body << sprintf("--%s\n", boundary)
	body << parts.join("--#{boundary}\n")
	body
      end
    end
  end
end

今回もユニットテスト活用して読みたいけど、通信が発生するようなメソッドはどうしたらいいのかな。

トラックバック - http://rubyist.g.hatena.ne.jp/muscovyduck/20060708

2006-07-07

[] QuickMLソースコードを読む(24) quickml/gettext.rbの続き  QuickMLのソースコードを読む(24) quickml/gettext.rbの続き - バリケンのRuby日記 を含むブックマーク はてなブックマーク -  QuickMLのソースコードを読む(24) quickml/gettext.rbの続き - バリケンのRuby日記  QuickMLのソースコードを読む(24) quickml/gettext.rbの続き - バリケンのRuby日記 のブックマークコメント

昨日の日記の続きだよ。

QuickML::GetText::Catalogクラス

    class Catalog
      def initialize (filename)
        load(filename)
        @messages = Messages
        @codeconv_method = CodeconvMethod
        @charset = Charset
      end
      attr_reader :messages
      attr_reader :codeconv_method
      attr_reader :charset
    end

これは、ローカライズのためのカタログファイルを抽象化したQuickML::GetText::Catalogクラスの定義だよ。initializeでKernel#loadメソッドでfilenameで指定したファイルをロードして実行しているよ。そのほかのメソッドはアクセサだけだから、とくに説明はいらないよね?

じゃあ、テストを書いてみよう!

require 'test/unit'
require 'kconv'
require 'quickml/gettext'

class TC_Catalog < Test::Unit::TestCase
  include QuickML::GetText

  def setup
    @catalog = Catalog.new('/usr/share/quickml/messages.ja')
  end

  def test_messages
    assert_equal @catalog.messages["<%s> was removed from the mailing list:\n<%s>\n"].tojis, "<%s> は\nメーリングリスト <%s> から削除されました。\n".tojis
  end

  def test_codeconv_method
    assert_equal @catalog.codeconv_method, :tojis
  end

  def test_charset
    assert_equal @catalog.charset, "iso-2022-jp"
  end
end

ユニットテスト万歳!

QuickML::GetText::MessageValidatorクラス

    class MessageValidator
      def initialize (catalog, source_filename)
        @catalog = catalog
        @source_filename  = source_filename
        @has_error = false
      end

      def read_file_with_numbering (filename)
        content = ''
        File.open(filename).each_with_index {|line, idx|
          lineno = idx + 1
          content << line.gsub(/\b_\(/, "_[#{lineno}](")
        }
        content
      end

      def collect_messages (content)
        messages = []
        while content.sub!(/\b_\[(\d+)\]\((".*?").*?\)/m, "")
          lineno  = $1.to_i
          message = eval($2)
          messages.push([lineno, message])
        end
        messages
      end

      def validate
        @catalog or return
        content = read_file_with_numbering(@source_filename)
        messages = collect_messages(content)
        messages.each {|lineno, message|
          translated_message = @catalog.messages[message]
          if not translated_message
            printf "%s:%d: %s\n", @source_filename, lineno, message.inspect
            @has_error = true
          elsif message.count("%") != translated_message.count("%")
            printf "%s:%d: %s => # of %% mismatch.\n",
              @source_filename, lineno, message.inspect, translated_message
            @has_error = true
          end
        }
      end

      def ok?
        not @has_error
      end
    end
  end
end

カタログがちゃんと適用されているかどうかをチェックする、QuickML::GetText::MessageValidatorクラスの定義だよ。

validateメソッドが実際のチェックを行う部分だから、ここではread_file_with_numberingメソッドとcollect_messagesメソッドの挙動を理解するためのユニットテストを書いてみるよ。

require 'test/unit'
require 'kconv'
require 'quickml/gettext'

class TC_MessageValidator < Test::Unit::TestCase
  include QuickML::GetText

  def setup
    @catalog = Catalog.new('/usr/share/quickml/messages.ja')
    @validator = MessageValidator.new(@catalog, 'test_source.rb')
  end

  def test_read_file_with_numbering
    assert_equal @validator.read_file_with_numbering('test_source.rb'), "\n _[2](\"hoge\")\n\n _[4](\"fuga\")\n"
  end

  def test_collect_messages
    assert_equal @validator.collect_messages(@validator.read_file_with_numbering('test_source.rb')), [[2, "hoge"], [4, "fuga"]]
  end

end

このテストのために、次のようなtest_source.rbファイルを用意したよ。


 _("hoge")

 _("fuga")

こちらも動作させてみれば理解できると思うから、実際にテストを走らせてみてね。

スクリプトとして起動したときに実行される部分

さあ、quickml/gettext.rbの最後の部分を読んでみよう。

if __FILE__ == $0
  include QuickML::GetText
  if ARGV.length < 2
    puts "usage: ruby gettext.rb <catalog> <source...>"
    exit
  end
  catalog_file = ARGV.shift
  catalog = Catalog.new(catalog_file)

  ok = true
  ARGV.each {|source_file|
    validator = MessageValidator.new(catalog, source_file)
    validator.validate
    ok = (ok and validator.ok?)
  }
  if ok then exit else exit(1) end
end

さいしょに「if __FILE__ == $0」とあるね。「__FILE__」は、「このファイル」つまりgettext.rbという意味だったね。「$0」は「起動したファイル」という意味だったね。だから「if __FILE__ == $0」とかくと、「このファイルで起動した場合」つまり「ruby gettext.rb」として起動した場合に以下を実行、という意味になるんだね。ひとつのファイルで「ライブラリ兼実行スクリプト」としたいときに便利だね。

引数を二つとって、カタログファイルソースファイルを指定すると、gettextメソッドを使っている部分をソースファイルからチェックして、カタログ置換が実施されていない部分があると、その部分を行番号つきで知らせてくれる、というわけだね。デバッグ用として便利かも。

今日はここまで。ユニットテストを使って、ちょっと端折ってquickml/gettext.rbを読み終わったね。こうやってユニットテストを使って端折って読めば、けっこう早く読めるかも。

トラックバック - http://rubyist.g.hatena.ne.jp/muscovyduck/20060707

2006-07-06

[] QuickMLソースコードを読む(23) quickml/gettext.rbの続き  QuickMLのソースコードを読む(23) quickml/gettext.rbの続き - バリケンのRuby日記 を含むブックマーク はてなブックマーク -  QuickMLのソースコードを読む(23) quickml/gettext.rbの続き - バリケンのRuby日記  QuickMLのソースコードを読む(23) quickml/gettext.rbの続き - バリケンのRuby日記 のブックマークコメント

昨日の日記の続きだよ。

じゃあ、最初から読んでいくね。

module QuickML
  module GetText
    module GetText
      def codeconv (text)
        if @catalog && @catalog.codeconv_method
          text.send(@catalog.codeconv_method)
        else
          text
        end
      end

これは、QuickML::GetText::GetText::codeconvメソッドの定義だよ。もしインスタンス変数@catalogがnil以外で、かつインスタンス変数@catalogが貼り付けてあるQuickML::GetText::Catalogクラスオブジェクトに対してcodeconv_methodメソッド(アクセサ)を依頼した結果もnilでないなら、引数で与えられたtextオブジェクトに対してObject#sendメソッドで@catalog.codeconv_methodの結果得られる文字列のメソッドを実行するよ。

      def gettext (text, *args)
        unless @catalog && @catalog.charset == @message_charset
          return sprintf(text, *args)
        end

        translated_message = @catalog.messages[text]
        if translated_message
          codeconv(sprintf(translated_message, *args))
        else
          sprintf(text, *args)
        end
      end

      def gettext2 (text)
        unless @catalog && @catalog.charset == @message_charset
          return text
        end

        translated_message = @catalog.messages[text]
        if translated_message
          codeconv(translated_message)
        else
          text
        end
      end

      alias :_ :gettext
      alias :__ :gettext2
    end

この二つのメソッドは似ているね。gettextメソッドは、まずunlessは「falseなら実行」だったよね。@catalogがnil、または@catalog.charset == @message_charsetがfalseであれば、sprintf(text, *args)をreturnするよ。そして@catalog.messages[text]で得た文字列にtranslated_message変数を貼り付けて、それがnilでなければ先ほど定義したcodeconvを、そうでなければsprintf(text, *args)をreturnするよ。

ちょっとこんな説明を聞いただけじゃ、どんな動作をするかイメージしにくいよね。そこでユニットテスト活用するよ。次のようなテストを書いてみたよ。

require 'test/unit'
require 'kconv'
require 'quickml/gettext'

class TC_GetText < Test::Unit::TestCase
  include QuickML::GetText

  def setup
    @catalog = Catalog.new('/usr/share/quickml/messages.ja')
    @message_charset = "iso-2022-jp"
  end

  include GetText
  def test_codeconv
    assert_equal codeconv('English'), 'English'
    assert_equal codeconv('日本語'), '日本語'.tojis
  end

  def test_gettext
    assert_equal _("<%s> was removed from the mailing list:\n<%s>\n", 'testuser', 'testml'), sprintf("<%s> は\nメーリングリスト <%s> から削除されました。\n", 'testuser', 'testml').tojis
  end

  def test_gettext2
    assert_equal __("because the address was unreachable.\n"), "メールが届かないためです。\n".tojis
  end
end

そして/usr/share/quickml/messages.jaの内容は次のようになっているよ。

# -*- mode: ruby -*-
Messages = {
  "<%s> was removed from the mailing list:\n<%s>\n" =>
  "<%s> は\nメーリングリスト <%s> から削除されました。\n",

  "because the address was unreachable.\n" =>
  "メールが届かないためです。\n",

  "ML will be closed if no article is posted for %d days.\n\n" =>
  "このメーリングリストは %d日以内に投稿がないと消滅します。\n\n",

  "Time to close: %s.\n\n" =>
  "消滅予定日時: %s\n\n",

  "%Y-%m-%d %H:%M" =>
  "%Y年%m月%d日 %H時%M分",

  "You are not a member of the mailing list:\n<%s>\n" =>
  "あなたは <%s> メーリングリストのメンバーではありません。\n",

  "You are removed from the mailing list:\n<%s>\n" =>
  "あなたは <%s> メーリングリストから削除されました。\n",

  "by the request of <%s>.\n" =>
  "<%s> が削除をお願いしたためです。\n",

  "You have unsubscribed from the mailing list:\n<%s>.\n" =>
  "あなたは <%s> メーリングリストから退会しました。",

  "The following addresses cannot be added because <%s> mailing list reaches the max number of members (%d persons)\n\n" =>
  "<%s> メーリングリストはメンバーの最大人数 (%d人)\nに達したので以下のアドレスは追加できませんでした。\n\n",

  "Invalid mailing list name: <%s>\n" =>
  "<%s> は正しくないメーリングリスト名です。\n",

  "You can only use 0-9, a-z, A-Z,  `.',  `-', and `_' for mailing list name\n" =>
  "メーリングリスト名には 0-9, a-z, A-Z, 「.」「-」「_」だけが使えます。\n",

  "Sorry, your mail exceeds the limitation of the length.\n" =>
  "申し訳ありません。あなたのメールのサイズは制限を超えました。\n",

  "The max length is %s bytes.\n\n" =>
  "メールのサイズの制限は %s バイトです。\n\n",

  "[%s] Unsubscribe: %s" =>
  "[%s] 退会: %s",

  "[%s] ML will be closed soon" =>
  "[%s] メーリングリスト停止のご案内",

  "[%s] Removed: <%s>" =>
  "[%s] メンバー削除: <%s>",

  "Members of <%s>:\n" =>
  "<%s> のメンバー:\n",

  "How to unsubscribe from the ML:\n" =>
  "このMLを退会する方法:\n",

  "- Just send an empty message to <%s>.\n" =>
  "- 本文が空のメールを <%s> に送ってください\n",

  "- Or, if you cannot send an empty message for some reason,\n" =>
  "- 本文が空のメールを送れない場合は、\n",

  "  please send a message just saying 'unsubscribe' to <%s>.\n" =>
  "  本文に「退会」とだけ書いたメールを <%s> に送ってください\n",

  "  (e.g., hotmail's advertisement, signature, etc.)\n" =>
  "  (署名やhotmailの広告などがついて空メールを送れない場合など)\n",

  "[QuickML] Error: %s" =>
  "[QuickML] エラー: %s",

  "New Member: %s\n" =>
  "新メンバー: %s\n",

  "Did you send a mail with a different address from the address registered in the mailing list?\n" =>
  "メーリングリストに登録したメールアドレスと異なるアドレスからメールを送信していませんか?\n",

  "Please check your 'From:' address.\n" =>
  "差出人のメールアドレスを確認してください。\n",

  "Info: %s\n" => 
  "使い方: %s\n",

  "----- Original Message -----\n" =>
  "----- 元のメッセージ -----\n",

  "[%s] Confirmation: %s" =>
  "[%s] 確認: %s",

  "Please simply reply this mail to create ML <%s>.\n" =>
  "このメールに返信すると <%s> メーリングリストが作られます。\n",

  "<%s> was created by <%s>\n" =>
  "<%s> が <%s>により作成されました\n"
}

Charset = "iso-2022-jp"
CodeconvMethod = :tojis

言語ライブラリと言うとすごい難しそうに思うけど、ソースコードを読んでみればハッシュを使ったとてもシンプルな実装になっていることがわかるよね。

あと、ユニットテストソースコードを読むのにも使えるねえ。

トラックバック - http://rubyist.g.hatena.ne.jp/muscovyduck/20060706

2006-07-05

[] QuickMLソースコードを読む(22) quickml/gettext.rb  QuickMLのソースコードを読む(22) quickml/gettext.rb - バリケンのRuby日記 を含むブックマーク はてなブックマーク -  QuickMLのソースコードを読む(22) quickml/gettext.rb - バリケンのRuby日記  QuickMLのソースコードを読む(22) quickml/gettext.rb - バリケンのRuby日記 のブックマークコメント

今日からは、QuickML組み込みの多言語ライブラリquickml/gettext.rbを読んでいくよ。

#
# quickml/gettext - a part of quickml server
#
# Copyright (C) 2002-2004 Satoru Takabayashi <satoru@namazu.org>
#     All rights reserved.
#     This is free software with ABSOLUTELY NO WARRANTY.
#
# You can redistribute it and/or modify it under the terms of
# the GNU General Public License version 2.
#

module QuickML
  module GetText
    module GetText
      def codeconv (text)
        if @catalog && @catalog.codeconv_method
          text.send(@catalog.codeconv_method)
        else
          text
        end
      end

      def gettext (text, *args)
        unless @catalog && @catalog.charset == @message_charset
          return sprintf(text, *args)
        end

        translated_message = @catalog.messages[text]
        if translated_message
          codeconv(sprintf(translated_message, *args))
        else
          sprintf(text, *args)
        end
      end

      def gettext2 (text)
        unless @catalog && @catalog.charset == @message_charset
          return text
        end

        translated_message = @catalog.messages[text]
        if translated_message
          codeconv(translated_message)
        else
          text
        end
      end

      alias :_ :gettext
      alias :__ :gettext2
    end

    class Catalog
      def initialize (filename)
        load(filename)
        @messages = Messages
        @codeconv_method = CodeconvMethod
        @charset = Charset
      end
      attr_reader :messages
      attr_reader :codeconv_method
      attr_reader :charset
    end

    class MessageValidator
      def initialize (catalog, source_filename)
        @catalog = catalog
        @source_filename  = source_filename
        @has_error = false
      end

      def read_file_with_numbering (filename)
        content = ''
        File.open(filename).each_with_index {|line, idx|
          lineno = idx + 1
          content << line.gsub(/\b_\(/, "_[#{lineno}](")
        }
        content
      end

      def collect_messages (content)
        messages = []
        while content.sub!(/\b_\[(\d+)\]\((".*?").*?\)/m, "")
          lineno  = $1.to_i
          message = eval($2)
          messages.push([lineno, message])
        end
        messages
      end

      def validate
        @catalog or return
        content = read_file_with_numbering(@source_filename)
        messages = collect_messages(content)
        messages.each {|lineno, message|
          translated_message = @catalog.messages[message]
          if not translated_message
            printf "%s:%d: %s\n", @source_filename, lineno, message.inspect
            @has_error = true
          elsif message.count("%") != translated_message.count("%")
            printf "%s:%d: %s => # of %% mismatch.\n",
              @source_filename, lineno, message.inspect, translated_message
            @has_error = true
          end
        }
      end

      def ok?
        not @has_error
      end
    end
  end
end

if __FILE__ == $0
  include QuickML::GetText
  if ARGV.length < 2
    puts "usage: ruby gettext.rb <catalog> <source...>"
    exit
  end
  catalog_file = ARGV.shift
  catalog = Catalog.new(catalog_file)

  ok = true
  ARGV.each {|source_file|
    validator = MessageValidator.new(catalog, source_file)
    validator.validate
    ok = (ok and validator.ok?)
  }
  if ok then exit else exit(1) end
end

じゃあ、ユニットテストの練習もいっしょにやりながら読んでいくよ。

require 'test/unit'
require 'kconv'
require 'quickml/gettext'

class TC_GetText < Test::Unit::TestCase
  include QuickML::GetText

  def setup
    @catalog = Catalog.new('/usr/share/quickml/messages.ja')
  end

  include GetText
  def test_codeconv
    assert_equal codeconv('English'), 'English'
    assert_equal codeconv('日本語'), '日本語'
  end
end

テストを実行するよ。

$ ruby gettext_test.rb

実行結果だよ。

Loaded suite gettext_test
Started
F
Finished in 0.192316 seconds.

  1) Failure:
test_codeconv(TC_GetText) [gettext_test.rb:15]:
<"\e$BF|K\\8l\e(B"> expected but was
<"\306\374\313\334\270\354">.

1 tests, 2 assertions, 1 failures, 0 errors

おおー、ユニットテストって面白いかも。

でもテストケースを考えるのって、けっこう頭使うかも。

トラックバック - http://rubyist.g.hatena.ne.jp/muscovyduck/20060705

2006-07-04

[] ユニットテスト  ユニットテスト - バリケンのRuby日記 を含むブックマーク はてなブックマーク -  ユニットテスト - バリケンのRuby日記  ユニットテスト - バリケンのRuby日記 のブックマークコメント

今日からTest::Unitを使ったユニットテスト勉強をしようと思うよ!

それにしても、リファレンスマニュアルがしっかり書かれているんだねえ。リファレンスマニュアルには、いつか全部目を通してみたいなあ。

トラックバック - http://rubyist.g.hatena.ne.jp/muscovyduck/20060704

2006-07-03

[][] Ruby on Rails専門学校を作るなら  Ruby on Railsの専門学校を作るなら - バリケンのRuby日記 を含むブックマーク はてなブックマーク -  Ruby on Railsの専門学校を作るなら - バリケンのRuby日記  Ruby on Railsの専門学校を作るなら - バリケンのRuby日記 のブックマークコメント

もしも「Ruby on Railsを使ったWebサイトの構築と、そのサイトの管理運用までをできる人材を養成する専門学校」を作るとしたら、どんなカリキュラムでどんな教科書を使うのがいいかなあ、なんてことを想像してみたら面白いかも。

Railsそのもの

やっぱりこの本かなあ。

RailsによるアジャイルWebアプリケーション開発

RailsによるアジャイルWebアプリケーション開発

でももうちょっとわかりやすい本が今後出てきそうだから、そっちに期待してもいいかも。

モデル設計(データベース設計)

モデル設計の教科書は、やっぱりこの2冊。

すらすらと手が動くようになるSQL書き方ドリル

すらすらと手が動くようになるSQL書き方ドリル

楽々ERDレッスン (CodeZine BOOKS)

楽々ERDレッスン (CodeZine BOOKS)

ビュー(というかデザイン

このへんはどんな本がいいのかなあ?Webデザインとかアクセシビリティユーザビリティ、Ajaxあたりの定番の教科書って何だろう?

コントローラーの実装

ここはやっぱりRubyの知識が生きてくるよね。

あとできればコーディングテクニックを鍛えるために、Ruby以外のプログラミング言語も比較しながら勉強したほうがいいかも。

その他

本番用のWebサーバの構築やデプロイなんかも考えると、LinuxとかのサーバOSカリキュラムもあったほうがいいよね。使い方だけじゃなくて理論も。このへんも定番の教科書ってあるかなあ。

あとはネットワークの知識も必要だよね。TCP/IPあたりからHTTPSSLSMTPCGIあたりまで。そうなってくると暗号に関する教科書も欲しいかも。

ハードディスクとかCPUメモリとかのハードウェアサーバ)に関する知識も要るかも。

トラックバック - http://rubyist.g.hatena.ne.jp/muscovyduck/20060703

2006-07-02

トラックバック - http://rubyist.g.hatena.ne.jp/muscovyduck/20060702

2006-07-01

[] QuickMLのソースコードを読む(21) quickml/sweeper.rbの続き  QuickMLのソースコードを読む(21) quickml/sweeper.rbの続き - バリケンのRuby日記 を含むブックマーク はてなブックマーク -  QuickMLのソースコードを読む(21) quickml/sweeper.rbの続き - バリケンのRuby日記  QuickMLのソースコードを読む(21) quickml/sweeper.rbの続き - バリケンのRuby日記 のブックマークコメント

昨日の日記の続きだよ。

    def shutdown
      until @status == :safe
	sleep(0.5)
      end
      @logger.vlog "Sweeper shutdown"
    end
  end
end

これはQuickML::Sweeper#shutdownメソッドだね。

インスタンス変数「@status」が張り付けてあるオブジェクトが「:safe」シンボルになるまで、Kernel#sleep(0.5)つまり0.5秒スクリプトを停止する、という動作を繰り返して、最後にスイーパーの停止をQuickML::Logger#vlogメソッドでログに記録しているよ。

今日はここまで。とりあえず今日でsweeper.rbは読み終わったね。

トラックバック - http://rubyist.g.hatena.ne.jp/muscovyduck/20060701