現在の~/bin/isbn.rb. だいぶ古い。 situated scriptなのでprefixが978で決め打ちだったりと暗黙の制約がある。

#!/usr/bin/ruby
# license: public domain

module ISBN
  module_function
  def normalize(src)
    src.gsub(/[^0-9X]/, '')
  end
  def calc_cd_13(src)
    digits = normalize(src)[0..11].split(//).map{|n| n.to_i}
    raise "digits not 12 but was #{digits}" unless digits.size == 12
    sum = 0
    digits.each_with_index{|d, i|
      coef = if i.odd? then 3; else 1; end
      sum = sum + (d * coef)
    }
    cdnum = 10 - sum.divmod(10).last
    case cdnum
    when  0 then "0"
    when 10 then "0"
    else cdnum.to_s
    end
  end
  def calc_cd_10(src)
    digits = normalize(src)[0..8].split(//).map{|n| n.to_i}
    raise "digits not 9 but was #{digits}" unless digits.size == 9
    sum = 0
    digits.each_with_index{|d, i|
      sum = sum + (d * (10 - i))
    }
    cdnum = 11 - sum.divmod(11).last
    case cdnum
    when 10 then "X"
    when 11 then "0"
    else cdnum.to_s
    end
  end
  def calc_cd(src, n = :unspecified)
    case n
    when 13 then calc_cd_13(src)
    when 10 then calc_cd_10(src)
    when :unspecified
      normalized = normalize(src)
      len = normalized.length
      case len
      when 13 then calc_cd_13(normalized)
      when 12 then calc_cd_13(normalized)
      when 10 then calc_cd_10(normalized)
      when  9 then calc_cd_10(normalized)
      else
        raise "cannot guess ISBN-13 or ISBN-10 from length #{len} (#{src})"
      end
    else
      raise "unknown digit specified: #{n}"
    end
  end
  def validate_length(src)
    len = normalize(src).length
    case
    when (len != 10 and len != 13) then return false
    else return src
    end
  end
  def validate_cd(src)
    normalized = normalize(src)
    if normalized.split(//).last == calc_cd(normalized)
      calc_cd(normalized)
    else
      false
    end
  end
  def validate(src)
    (validate_length(src) and validate_cd(src))
  end
  def isbn(src, n = 13)
    raise "invalid src length: #{src}" unless validate_length(src)
    normalized = normalize(src)
    dummy_cd = "0"
    case n
    when 13
      case normalized.length
      when 13 then without_cd = normalized.chop
      when 10 then without_cd = "978" + normalized.chop
      else
        raise "invalid digits size: #{normalized.length} for #{src}"
      end
      without_cd + calc_cd_13(without_cd + dummy_cd)
    when 10
      case normalized.length
      when 13 then without_cd = normalized[3..-1].chop
      when 10 then without_cd = normalized.chop
      else
        raise "invalid digits size: #{normalized.length} for #{src}"
      end
      without_cd + calc_cd_10(without_cd + dummy_cd)
    else
      raise "invalid digits size #{normalized.length} specified for #{src}"
    end
  end
end

if $0 == __FILE__
  require 'optparse'
  default_config = {
    :digit => 13,
    :test  => false
  }
  clo = command_line_options = {}
  ARGV.options {|o|
    o.def_option('--digit=NUM', 'specify output digit'){|n| clo[:digit] = n.to_i}
    o.def_option('--test', 'run test'){clo[:test] = true}
    o.def_option('--help', 'show this message'){puts o; exit(0)}
    o.parse!
  } or exit(1)
  config = default_config.update(clo)
  if config[:test]
    require 'test/unit'
    class TC_ISBN < Test::Unit::TestCase
      def setup
        @isbn10ok1 = "ISBN4-10-109205-2"
        @isbn10ng1 = "ISBN4-10-109205-3"
        @isbn13ok1 = "ISBN978-4-10-109205-8"
        @isbn13ng1 = "ISBN978-4-10-109205-9"
        @isbn10ok2 = "4274067335"
        @isbn10ng2 = "4274067336"
        @isbn13ok2 = "9784274067334"
        @isbn13ng2 = "9784274067337"
        @isbns = [@isbn10ok1, @isbn10ng1, @isbn13ok1, @isbn13ng1,
                  @isbn10ok2, @isbn10ng2, @isbn13ok2, @isbn13ng2]
      end
      def test_calc_cd
        expected = ["2", "2", "8", "8", "5", "5", "4", "4"]
        result = @isbns.map{|s| ISBN.calc_cd(s)}
        assert_equal(expected, result)
      end
      def test_validate
        expected = ["2", false, "8", false, "5", false, "4", false]
        result = @isbns.map{|s| ISBN.validate(s)}
        assert_equal(expected, result)
      end
      def test_isbn_13
        expected = ["9784101092058",
                    "9784101092058",
                    "9784101092058",
                    "9784101092058",
                    "9784274067334",
                    "9784274067334",
                    "9784274067334",
                    "9784274067334"]
        result = @isbns.map{|s| ISBN.isbn(s, 13)}
        assert_equal(expected, result)
      end
      def test_isbn_10
        expected = ["4101092052",
                    "4101092052",
                    "4101092052",
                    "4101092052",
                    "4274067335",
                    "4274067335",
                    "4274067335",
                    "4274067335"]
        result = @isbns.map{|s| ISBN.isbn(s, 10)}
        assert_equal(expected, result)
      end
    end
  else
    result = ISBN.isbn(ARGF.read, config[:digit])
    if $stdout.tty?
      puts result
    else
      print result
    end
  end
end

使い方。標準入出力でやり取りする。

% isbn.rb --help
Usage: isbn [options]
        --digit=NUM                  specify output digit
        --test                       run test
        --help                       show this message

% echo 1234567890 | isbn.rb --digit 10
123456789X
% echo 1234567890 | isbn.rb --digit 13
9781234567897
% echo 1234567890 | isbn.rb
9781234567897

Emacsからはこんな感じで使う。 ISBN13を囲んでこうするとISBN10に置き換わる。

(call-process-region
  (region-beginning) (region-end)
  "ruby"
  t; DELETE
  t; BUFFER
  nil; DISPLAY
  "/home/johnd/bin/isbn.rb" "--digit=10")