おもこん

おもこんは「思いつくままにコンピュターの話し」の省略形です

拡張テンパズル

YoutubeのQuizKnockを見ていて、面白い問題を人間とコンピュータ(のプログラムを作る人間)が勝負していました。 動画はもちろん面白かったのですが、このパズルを解くプログラムを自分でも作ってみたくなりました。

このパズルは例えば次のようなものです。

数字1, 2, 3, 4, 5と四則演算(加減乗除のこと)をつかい、その式の計算結果が20になるような式を見つけよ。

答えは複数ある可能性がありますが、ひとつ見つければ良いことにします。 例えば、

 3\times 5-1+2+4=20

はひとつの答になっています。

パズルの名前

このパズルは有名なのでしょうか?またその呼び名はあるのでしょうか? ネットを探してみると、「テンパズル」または「メイクテン」というのが、これに似ています。

ただし、テンパズルは与えられる一桁の数字が4個で、計算結果は10に固定されています。 計算結果が10ということが「テン」パズル、あるいはマイク「テン」の元になっているようです。

拡張テンパズル

このパズルの条件を拡張します。

  • 計算式に使う数字は4個でなくても良い。2個以上なら良いものとする
  • 計算式に使う数字は二桁以上でも良い。要するに自然数なら良い
  • 計算結果は10でなくても良い

これをここでは「拡張テンパズル」と呼ぶことにします。 拡張テンパズルを解くプログラムをRubyで作ってみました。 GitHubにすでにアップロードしてあります。

(9/29 追記)

このレポジトリのプログラム全体が大きく変更されました。 そのため、以下の記事とレポジトリの説明文書が異なる部分があります。 その場合は、レポジトリ内の文書の方が正しいです。 また、変更後のプログラムはgem形式になっていて、requireするときは、ライブラリ全体を対象に行います。

require 'math_programs'

下記のプログラムのrequire_relative文は、このように置き換えるなどしてみてください。

(追記 終わり)

プログラム名の「e10p.rb」がextended TenPuzzleからきています。 このプログラムはライブラリであり、メインプログラムを作る必要があります。 demo.rbがそのような例になっているので、参考にしてください。

プログラムの実行

プログラムを実行するためのメインプログラムの例を示します。

require_relative "e10p.rb"

numbers = ARGV.dup.map{|s| s.to_i}
sum = numbers.pop
puzzle = E10P.new(numbers, sum)
print "#{puzzle.solve}\n"

これを実行するときに、引数に「式に使う数字」と「合計」を空白区切りで指定します。 例えば、式に使う数字が11, 20, 33で、合計が42とすると、

$ ruby main_program.rb 11 20 33 42
33-(11-20)

このように答えが表示されます。

テンパズルもやってみましょう。

$ ruby main_program.rb 2 3 5 8 10
5*(8-2*3)

面白いのは、分数が一度出てくるような例があることです。

$ ruby main_program.rb 1 1 9 9 10
9*(1+1/9)

プログラムの仕組み

例 2,4,5,6から10を作る

答えの一例は

 5\times(2\times 4-6)

です。 これは木構造で表すと

  *
 / \
5   -
   / \
  *   6
 / \
2   4

となります。 一番下の階層は2*4になっています。 この結果は8ですが、これから、この問題は、8と5, 6の3つで10になる計算を見つければ良いことになります。 もちろん、最初から2と4を組み合わせれば良いことは分かりませんから、4つの数字から2個をとる順列を生成し、虱潰しに探していきます。

数字がひとつ減ったので、今と同じ方法を用いると、次は数字が2つになります。 このようにして数字を減らしていって、計算結果が10になる場合を見つければ良いことになります。 これは、再帰呼出しです。

GitHubのプログラムでは、solve_realというメソッドがそれに当たります。

この問題はプログラム化が結構難しいと思いますが、Rubyのクラスを導入して木構造を作ると短くまとめることができます。

WindowsにImageMagickとRMagickをインストール

Windowsで画像処理を行うため、ImageMagickをインストールします

ImageMagickのインストール

  • ImageMagickのウェブサイトをブラウザで開く
  • タイトルバーのダウンロードボタンをクリック(他に広告のダウンロードボタンもあるが、それは違うので要注意)
  • 下の方にWindowsインストーラがある。「ImageMagick-7.1.1-15-Q16-HDRI-x64-dll.exe」をクリックしてダウンロード。 PCの能力に応じて別のインストーラを選択してもよい
  • インストーラの指示に従ってインストール。 途中で「Install development headers and libraries for C and C++」にチェックを入れる。 これはRMagickを後にインストールするときに必要なため

テスト

  • 「ドキュメント」フォルダを開き、右クリックから「ターミナルで開く」をクリック
  • 「magick logo: logo.gif」と入力。ドキュメント・フォルダにlogo.gifというファイルができる。
  • フォルダ画面からlogo.gifをダブルクリック。ImageMagickのロゴが表示される

以上が確認できれば正常にインストールできています。

RMagickのインストール

RMagickはRubyのGem(ライブラリのこと)で、RubyImageMagickを使うためのものです。 使い方はオリジナルのImageMagickとは違い、Rubyプログラムに適した形になっています。 詳細はRMagickのドキュメントを参照してください。

ターミナルからgemコマンドでインストールします。

> gem install rmagick
Temporarily enhancing PATH for MSYS/MINGW...
Building native extensions. This could take a while...
Successfully installed rmagick-5.3.0
Parsing documentation for rmagick-5.3.0
Installing ri documentation for rmagick-5.3.0
Done installing documentation for rmagick after 2 seconds
1 gem installed
>

RMagickのテスト

エディタで次のプログラムを作成し、test.rbのファイル名で保存します。 保存先のディレクトリはどこでも良いのですが、一応ドキュメント(Documents)にしておきましょう。

require 'rmagick'
include Magick

img = Image.new(800,600) do |options|
    options.background_color = 'blue'
end
img.write("blue.png")

端末から実行します。 まず、カレント・ディレクトリがドキュメントになっていることを確認しておいてください。

> ruby test.rb
>

同じフォルダにblue.pngができているはずです。 それは800x600サイズで全面青色の画像ファイルです。 それができていれば、RMagickはきちんと動作しています。

RMagickの応用

RMagickを使ってプロジェクターの画面解像度に写真サイズを合わせるプログラムが、 「プロジェクター用スライドの画像について」にありますので、参考にしてください。

この他にもいろいろなことができるので、RMagickのドキュメントを参考に試してみると良いと思います。

WindowsにRubyをインストール

関係者との互換性のため、Windowsでの作業が増えてきました。 しかし、WindowsLinuxと比べツールが少なく、使い勝手がよくありません。 それを解消するための手段のひとつとして、Rubyをインストールすることにしました。

Rubyのウェブサイトからダウンロード

  • Rubyのウェブサイトをブラウザで開き、「ダウンロード」ボタンをクリック
  • Rubyのインストール方法」を見ると「Windowsマシンでは、RubyInstallerといったツールが使えます。」とある
  • その文の中にある「RubyInstaller」をクリック
  • 「RubyInstaller」という別サイトが開く
  • 「Download」ボタンをクリック
  • Ruby+Devkit 3.2.2-1 (x64)」をクリック=>ダウンロード開始(バージョンは2023/8/26時点)
  • ダウンロードしたインストーラをダブルクリック。以下指示に従いインストールする

エディタのダウンロード

Rubyプログラムを書くためのエディタをインストールします。 私は、Visual Studio Codeをインストールすることにしました。 もちろん他のエディタ、例えばAtomなどでも良いと思います。

  • Visual Studio Codeのウェブサイトをブラウザで開く
  • Visual Studio Codeをダウンロードする」ボタンをクリック
  • Windows」のボタンをクリック。別画面に遷移すると同時にダウンロードも行われる
  • インストーラをダブルクリックして起動。以下指示に従う。私は「デスクトップにアイコンを作成」にもチェックを入れた

テスト

アプリケーションが動くかどうかテストします。 エディタを立ち上げ、次のプログラム「hello.rb」を作り、保存します。

print "Hello world.\n"

そのフォルダで右クリックし「ターミナルで開く」をクリック。 ターミナルが開き、そのカレント・フォルダがプログラムのあるフォルダになっています。 次のようにコマンドから入力します。 なお、プロンプトは、フォルダのパス名に「>」がついたものになりますが、以下では単に「>」のみとします。

> ruby hello.rb
Hello world.
>

Hello world.が表示されればRubyは動作しています。

Rubyチュートリアル

Rubyの使い方については徒然なるままにRubyというチュートリアルを書いていますので参考にしてください。

BOMについて

WindowsRubyプログラムを作るときに問題となるBOMについて書いておきます。 BOMは「Byte Order Mark」、すなわちバイト順を表すマークでファイルの先頭にあります。 このマークはバイト順だけでなく、Unicodeのどれを使うかも示しています。 「バイトオーダーマークの使用」を参考にしてください。

例えば端末から

> echo abc >test.rb
>

とすると、ファイルの先頭に十進で「254, 255」というデータが入ります。 十六進では「FE, FF」です。 これが表すのは、UTF-16でビッグエンディアンの形式でデータが表されるということです。 RubyはBOM無しのUTF-8をプログラムのエンコーディングとしているので、test.rbを実行するとエラーになります。

以上から、Rubyのプログラムを作るときにはBOM無しのUTF-8を出力するようエディタを設定しておいてください。 VSCodeはデフォルトでそのようになっています。 ただし、BOM付きのファイルを読み込んで編集する場合は、上書き保存も同じBOMつきになります。 ですから、最初からVSCodeでファイルを作成することをお勧めします。

このエラーは見た目には良く分からないので、対策を立てにくいものです。

プロジェクター用スライドの画像について

プロジェクター用スライドの画像

プレゼンテーションでスライドを作るときに、画像の画素数(これを解像度と言うこともあるが、正しくはない)をどれくらいにしたら良いのでしょうか? 一般にデジカメで撮った画像は画素数が多いので、それを何枚もスライドに入れると容量が大きくなってしまいます。 また、画素数の多い写真はピントが合うまでに少々時間がかかることもあります。 プロジェクタの解像度に適するように変換が行われるためです。 そこで、スライドを作るときの最良の画素数を計算する方法を考えてみました。

結論的には、「プロジェクターの画面解像度が1920x1080ならば、画面いっぱいに写す画像も1920x1080が最適」ということです。 これだけで十分な人は以下の記事を読む必要はありません。

素数とは

デジカメやスマホの写真データは、色情報を持った小さな点の集合になっています。 この点を画素(英語でピクセルpixel)といいます。 写真データにおける画素の数を画素数といいますが、縦横を別々にして1600x900のように表すことが多いです。 この場合、1600が横で900が縦です。 総画素数は、1600x900=1440000=144万画素、となります。

画素の縦横比を「ピクセルアスペクト比」といいます。 画素が正方形なら1:1です。 画素が長方形のこともあり、例えばNTSCビデオシステムでは10:11です(ウィキペディアのピクセルアスペクト比参照)。 コンピュータのディスプレイなどでは、ピクセルアスペクト比は1:1と考えて差し支えありませんが、実際のディスプレイには若干の誤差があります。

解像度と画面解像度

同じ大きさの写真でも、画素が多いものと画素が少ないものでは見た目が大きく違います。 これは長さに対する画素数の割合、すなわち密度の問題になります。 解像度を表すにはDPI(ドット・パー・インチ)を用いることが多いです。 1インチ(2.54cm)あたりに画素がいくつあるかを示します。

DPIは長さに対する画素数の密度ですが、縦横でDPIが異なる可能性があります。 それはピクセルアスペクト比が1:1でない場合です。 それでは、パソコンの場合はどうでしょうか? パソコンディスプレイではピクセルアスペクト比は1:1ですから、DPIは縦横が同じになります。 そこで、縦横区別せずに単にDPIをひとつだけ示せば足りることになります。

一般に解像度が高いほうが見た目が良くなります。 ただし、人間の目には限界がありますので、ある程度の解像度を越えるとその違いは感じられなくなります。 また、ディスプレイとプリンタでは、その仕組みが違うので、同じ解像度でも見た目が変わります。 ちなみに私の持っているPCのディスプレイは13.3型(対角線の長さが13.3インチ)で1920x1080ピクセル(16:9)なので、 横の長さをxとすると

 \displaystyle
x^2+\left(\frac{9x}{16}\right)^2 = 13.3^2 \\
x = 11.6 \\
1920\div 11.6 = 165.6

これより約166dpiです。 解像度はディスプレイによって異なります。

解像度に似た用語に「画面解像度」があります。 これはディスプレイの縦横の画素数を示したもので、密度を表すわけではありません。 例えば私の持っているノートパソコンのディスプレイの画面解像度は1920x1080ドットです。 パソコンをプロジェクターにつなげる場合は、パソコンの外部出力の画面解像度とプロジェクタの画面解像度の能力で決まってきます。 多くの場合はプロジェクターの画面解像度になると思います。 例えば、私の持っているエプソンのプロジェクター「EF-100B」の場合は1920x1080です。

デジカメやスマホ・カメラの画素数

私の持っているデジカメはNikonの「COOLPIX B600」で、通常の撮影における画素数は4608×3456です。

 \displaystyle
4608 = 2^9\times 3^2 \\
3456 = 2^7\times 3^3 \\
4608:3456 = 4:3

同じくスマホのカメラは4000x2250です。 しかし、このスマホには4つのカメラ(1つはインカメラ・・・自撮り用カメラ)があるので、それぞれに画素数が異なります。 ですので、4000x2250はそのうちのひとつにおける画素数です。

 \displaystyle
4000 = 2^5\times 5^3 \\
2250 = 2\times 3^2\times 5^3 \\
4000:2250 = 16:9

いずれもディスプレイの画面解像度よりも画素数が多いです。 あた、アスペクト比(縦横比)が異なるので、4:3の画像は左右に黒い背景を追加して、中央に画像を置く必要があります。

EXIFデータ

EXIFデータはJPEGなどの画像に追加できるメタデータです。 撮影日時、場所、カメラなどの情報が良く用いられます。 ネット上に画像をあげるときに、これらの情報(個人情報)は削除する方が好ましいと言われています。

画像変換のプログラム

画像変換にはImagemagickを使うと良いです。 ここでは、そのRubyライブラリであるRMagickを使って変換プログラムを作ってみます。 私のプロジェクターの画像解像度が1920x1080なので、画像サイズもそれに合わせます。 読者がこのプログラムを活用する際にはお持ちのプロジェクターの画像解像度に合わせて調整してください。 また、16:9以外の画像をこのサイズ内におさめると余白ができますが、余白は黒にします。

ImagemagickやRMagickについては次のリンクが参考になります。

次のプログラムは最後の3行で与えられるファイル(sample1.jpg、sample2.jpg、sample3.jpg)を変換します。 EXIFデータは削除され、サイズが1920x1080に変更され、photo-0.png、photo1-.png、photo2-pngというファイルに保存されます。 定数WidthとHeightを書き換えることにより、異なるサイズに対応できます。

プログラムを起動するには、コマンドラインから

$ ruby image_for_slide.rb

とタイプします。

# image_for_slide.rb 
#
# This program converts images for slides.
# - The image filenames are given after the __END__ line.
# - Removes EXIF data.
# - Resizes the images to suit slides
# - The name of the created image files are "photo-0.png", "photo-1.png", ...
 
require 'rmagick'
include Magick

# Display resolution
# 16:9
Width = 1920
Height = 1080
# 4:3
# Width = 1024
# Height = 768

imagelist = ImageList.new(*DATA.read.split(/\n/))
imagelist.each do |image|
  image.strip!
end
imagelist = imagelist.map do |image|
  base = Image.new(Width,Height) {|options| options.background_color = 'black'}
  src = image.resize_to_fit(Width,Height)
  base.composite(src,CenterGravity, OverCompositeOp)
end
imagelist.write("photo.png")
__END__
sample1.jpg
sample2.jpg
sample3.jpg

徒然Ruby(42)gemを作って公開してみた

lbtというgemを作って公開してみた

以前からLaTeXで効率的に書籍を作ることを考えていました。 特に大きな文書、例えば100ページを越えるような文書ではタイプセットに時間がかかるのが問題です。 それを解決するには

  • 文書を複数のファイルに分ける
  • ひとつのファイルだけをタイプセットしてその出来栄えをチェックする。 これによって、タイプセットの時間を短縮できる

ということが必要です。

そのためのツールとしてLaTeX-Buildtoolsというプログラム群を作ってきました。 最初のバージョンはBashスクリプト、2番目はRubyとRakeを使ったものでした。 今回、3番めのバージョンとしてgemにすることを考えました。 それによって、ツールのインストールが格段に易しくなるからです。

$ gem install lbt

この1行だけでインストールが完了します。

今回の記事は、この作業で得た知見をもとに、gemのビルドと公開について書きたいと思います。 なお、RubyGems.orgのガイドに分かりやすいチュートリアルがあるので、そちらをご覧になるのも有益です。

lbtはどんなgemか

本題に入る前にlbtがどんなものかを説明します。

$ lbt new sample

これでsampleフォルダができ、その中にmain.texやhelper.texといったテンプレートが生成されます。 テンプレート内のタイトルや著者を書き直します。 そして本文部分をchap1/sec1.tex、chap1/sec2.texなどのファイルに、セクションごとに作っていきます。 なお、「chap数字」は章を表すディレクトリで、「sec数字.tex」はセクションのファイルです。 ファイル構成についてはGitHubのLbtのrakeバージョン・ブランチのReadme.mdを参考にしてください。 これができあがったらPDFファイルを生成します。 sampleフォルダをカレント・ディレクトリにして

$ lbt build

なお、ソースファイルはMarkdownも可能です。

ファイルの配置

gemを作るには特定のファイル配置をしなければなりません。

$ tree
.
├── License.md
├── README.md
├── Rakefile
├── Tutorial.en.md
├── Tutorial.ja.md
├── bin
│   └── lbt
├── lbt.gemspec
├── lib
│   ├── lbt
│   │   ├── build.rb
│   │   ├── create.rb
│   │   ├── part_typeset.rb
│   │   ├── renumber.rb
│   │   └── utils.rb
│   └── lbt.rb
└── test
    ├── test_build.rb
    ├── test_create.rb
    ├── test_lbt.rb
    ├── test_num2path.rb
    ├── test_part_typeset.rb
    ├── test_renumber.rb
    ├── test_utils1.rb
    └── test_utils2.rb

これがlbtディレクトリ構成です。 ポイントになるのは、

  • License.md、README.md、Rakefilelbt.gemspecをトップディレクトリに置く
  • 実行ファイル(lbt)はbinディレクトリの下に置き、実行可能属性をオンにする(chmodで755にすればよい)
  • libディレクトリの下にlbt.rb、つまり「gem名.rb」というファイルを置き、このファイルを通して下位ファイルをrequireないしrequire_relativeで取り込む
  • libディレクトリの下にlbtディレクトリを置き、その中に下位ファイルを置く
  • testディレクトリの下にテスト用ファイルを置く

以上から、本体のプログラムは、bin/lbt、lib/lbt.rb、lib/lbtディレクトリ下の諸ファイル、になります。

lbt.gemspec

「gemの名前.gemspec」というファイル(上記ではlbt.gemspec)がgemの内容を定義するファイルです。

Gem::Specification.new do |s|
  s.name              = 'lbt'
  s.version           = '0.5'
  s.summary           = 'LaTeX Build Tools'
  s.description       = 'Lbt is a build tool for LaTeX. It is useful for big documents.'
  s.license           = 'GPL-3.0'
  s.author            = 'XXXX XXXX'
  s.email             = 'XXXXXX@XXXXl.com'
  s.homepage          = 'https://github.com/ToshioCP/LaTeX-BuildTools'
  s.files             = ['bin/lbt', 'lib/lbt.rb', 'lib/lbt/build.rb', 'lib/lbt/create.rb', 'lib/lbt/part_typeset.rb', 'lib/lbt/renumber.rb', 'lib/lbt/utils.rb']
  s.executables       = ['lbt']
end

名前、バージョン、要約、説明、ライセンス、著者、連絡先email、ホームページ、gemに取り込むファイルの配列、実行ファイル名を指定しています。 この他にも設定項目を設けることができるので詳細はRubyGems.orgのガイドを参照してください。

Rakefile

Lbtでは、Rakefileにドキュメント生成(RDoc)とテストについて記述しました。 これに加えて、gemのビルドを記述することもできます。 Rubyのドキュメントを参考にしてください。

require "rdoc/task"
require "rake/testtask"

RDoc::Task.new do |rdoc|
  rdoc.main = "README.md"
  rdoc.title = "LaTeX-Buildtools"
  rdoc.rdoc_dir = "doc"
  rdoc.rdoc_files.include("README.md", "License.md", "Tutorial.en.md", "Tutorial.ja.md", "lib/lbt.rb", "lib/lbt/*.rb")
end
task :rdoc do
  touch "doc/.nojekyll"
end

Rake::TestTask.new do |t|
  # t.libs << "test"
  t.test_files = Dir.glob("test/test_*")
  t.verbose = true
end

RDoc::Task.new以下がドキュメント作成タスクを生成し、Rake::TestTask.new以下がテストの実行タスクを生成します。 コマンドラインからは、rdoc、testをrakeの引数にすることでそれぞれのタスクを実行します。

$ rake rdoc #=>ドキュメントを生成
$ rake test #=>テストを実行

ドキュメントやテストの内容は省略しますが、興味のある方はGitHubレポジトリを参照してください。

gemのビルド

gemをビルドするには、gemコマンドを用います。

$ gem build lbt.gemspec

これにより、カレントディレクトリにlbt-0.5.gemが出来上がります。 このファイルからgemをインストールするには

$ gem install ./lbt-0.5.gem

とします。 インストールが完了すると、lbtコマンドが実行できるようになります。 例えば

$ lbt new sample

でsampleディレクトリを生成し、テンプレートをその下に作ります。

RubyGems.orgへのアップロード

RubyGems.orgにgemをアップロードすることにより一般に公開することができます。 他のユーザは

$ gem install lbt

という1行でlbtをインストールできるようになります。

アップロードは次の手順で行います。

  • RubyGems.orgにサインアップ(ユーザ登録)する(サインアップはRubyGems.orgのウェブ画面から行う)
  • gem push (gemファイル名)でアップロードする(その時登録したユーザ名とパスワードが必要)

以上、gemの作成と公開の手順を紹介しました。 みなさんもRubyの有用なアプリやライブラリを持っていたらぜひGemとして公開してください。

徒然Ruby(41)エンコーディング

文字列のエンコーディングに頭を悩ませることはほとんどなくなりました。 なぜなら、どのアプリ、システムもUTF-8を使うようになったからです。 Rubyでもエンコーディングの問題が起こることはまず無いでしょう。 ですが、今回はエンコーディングの考え方を整理してみたいと思います。

ASCIIコード

コンピュータの内部では文字を数字に置き換えて記憶しています。 これを文字コードといいます。 初期の有名な文字コードにASCII(アスキー)がありますが、これは7ビットで表すことができます。 ビットとは、メモリーの最小単位で、1と0を区別できるものです。 8個のビットをバイトといい、コンピュータはバイト単位でメモリーを扱います。 1ビットは0と1を表すことができますが、1バイトだと2^8=256個を区別でき、数字としては0から255までを区別できるようになります。 ASCIIは7ビットなので、0から2^7-1=127までの数字に文字が対応します。

ASCII - Wikipedia

例えば大文字のAは16進数の41(10進数の65)小文字のaは16進数の61(10進数の97)などです。 詳しくは上記のリンク先を参照してください。 ASCIIで表せるのは大文字と小文字のアルファベット、ピリオドなどの記号、改行などを表すコントロールコードだけです。 要するに、キーボードで直接入力できる文字だと思えば良いでしょう。

ASCIIは7ビットですが、コンピュータはバイト単位にデータを処理するので、ASCIIも8ビットで処理されることが普通です。 このとき、最上位ビットは0になります。 もしも最上位ビットが1だと、ASCIIの定義外なので、文字としては不定ということになります。 Rubyではこのような1バイト単位で、0から127まではASCIIとして扱うことができるコード体系(エンコード)をASCII_8BITとしています。 主にバイナリデータを扱うのに使われます。

ASCII 以外のコード

日本語にはアルファベット以外に、ひらがな、カタカナ、漢字があります。 他の言語でも、例えばドイツ語ではウムラウトエスツェット(ß)があります。 これらをASCIIで表すことはできません。 そのため、2バイト以上を使って様々な文字と数字(文字コード)を対応させるということが考えられました。 この方法が現在ではUTF-8でほぼ統一されていますが、過去にはSHIFT-JISやEUC-JPなどがありました。 それらをエンコーディングといいます。 つまり、エンコーディングは文字と数字(文字コード)の対応を表すルールなのです。

しかし、UTF-8、SHIFT-JIS、EUC-JPには互換性がありませんので、あるコード体系から別のコード体系には「変換」が必要です。 過去にはWindowsはSHIFT-JISが使われLinuxではEUC-JPが使われていましたので、両者でデータのやりとりをするときには文字コードの変換が必要でした。 また、これらのコードは日本語以外の言語(ASCII以外)の文字サポートがありませんでした。 最終的にはUnicodeという様々な国の言語の文字をサポートするコード体系が生まれ、特にUTF-8が標準的に用いられるようになりました。 現在ではWIndowsLinuxMacUTF-8が標準です。

このようにしてUTF-8がどのシステムでも使われるようになったので、問題は起こらなくなりました。 これらの文字コードのことをエンコーディングといいます。 Rubyでは文字列にエンコーディングが付随していて、UTF-8以外にEUC-JPやSHIFT-JISにも対応できるようになっています。

以下ではRubyエンコーディングが問題になることがらについて説明します。

スクリプトエンコーディングリテラルエンコーディング

Rubyで書いているプログラム自体の文字コードはどのような問題を含むでしょうか? これは「スクリプトエンコーディング」の問題と呼びます。

Rubyのキーワードは、すべてASCIIの範囲にあり、その限りではRubyは正しくスクリプトを解釈してくれます。 UTF-8、SHIFT-JIS、EUC-JPなどは、すべてASCIIの範囲の文字はそのとおりにコードになっています。 例えば「def」の文字コードは16進数で「64 65 66」で、これは上記の3つの文字コードでも同じです。 このようにASCIIの範囲の文字はASCIIと同じコードを使うエンコーディングをASCII互換エンコーディングといいます。 それ以外のエンコーディングはASCII非互換エンコーディングです。

RubyスクリプトにはASCII互換エンコーディングRubyがサポートしている)を使うことができます。 逆にそれ以外、ASCII非互換やRubyがサポートしないエンコーディングは使うことができません。

また、スクリプトエンコーディングを明示的に指定したいときはマジックコメントを使います。 詳しくはRubyのドキュメントの多言語化を参照してください。

文字列リテラル正規表現リテラル、シンボルリテラルには文字列が出てくるので、エンコーディングが関わります。 これらはスクリプトエンコーディングに従います。 ただしバックスラッシュ記法で文字コードを表す場合は他の文字コードに変換されたり、エラーになることがあります。 詳細は多言語化を参照してください。 通常はリテラルエンコーディングスクリプトエンコーディングだと考えれば大丈夫です。

文字列のエンコーディング

Rubyの文字列オブジェクトはエンコーディングを持っていて、encodingメソッドでその文字列のエンコーディングを知ることができます。

p s.encoding #=>#<Encoding:UTF-8>
  • ある文字列を他のエンコーディングの同じ文字列に変更するにはencodeメソッドを使います。
  • ある文字列をその文字列の内容を変えずにエンコーディングを変更するにはforce_encodingメソッドを使います。

この2つは混乱しやすいので注意してください。 encodeメソッドはエンコーディングを変えるだけでなく、文字列の内容(コード)も変更しますが、force_encodingではエンコーディングのみが変更されます。

s = ""
p s.encoding #=>#<Encoding:UTF-8>
t = s.encode(Encoding::EUC_JP)
p t.encoding #=>#<Encoding:EUC-JP>
p s.force_encoding(Encoding::ASCII_8BIT)
p t.force_encoding(Encoding::ASCII_8BIT)

これを実行すると

#<Encoding:UTF-8>
#<Encoding:EUC-JP>
"\xE3\x81\x82"
"\xA4\xA2"

となります。 後半2行から、sとtでは文字コードが変更されていることが分かります。 異なるコードですが、表している文字は両方とも「あ」です。

文字列を==で比較する場合、「等しい」と判定されるのは次の2条件を満たすときです。

したがって、sとtは両方とも「あ」を表しているが、エンコーディングが異なるので、「s==t」はfalseになります。

このような問題は複数のエンコーディングを使っているところから発生するので、ひとつのエンコーディングだけならば、ことは単純になります。

I/Oのエンコーディング

外部から入力するときに、それが文字列であればエンコーディングが問題になります。

テキストの読み込み

テキスト読み込みメソッド、例えばIO.readlinesエンコーディングの影響を受けます。 読み込み元はRubyの外部ですから、Rubyエンコーディングを決めることはできません。 プログラマーが外部のエンコーディングを把握して、Rubyに設定することになります。 このエンコーディングをIOの「外部エンコーディング」といいます。

Rubyの内部で使っているエンコーディングはデフォルトでUTF-8です(変更もできますが)。 これを「内部エンコーディング」といいます。 あるIOに対して「外部エンコーディング」と「内部エンコーディング」がわかっていれば、Rubyは読み込み時に変換してくれます。 これらのエンコーディングを指定するのが「set_encoding」メソッドです。 その引数は、外部エンコーディング、内部エンコーディングの順に指定します。 エンコーディングには文字列またはエンコーディング定数(例えばEncoding::UTF_8)が使えます。

今、「こんにちは」という日本語テキストがEUC-JPで保存されたファイル「gr_euc.txt」があるとします。 これを読むこむときにUTF-8に変換するには次のようにします。

f = File.open("gr_euc.txt", "r")
f.set_encoding("EUC-JP", "UTF-8")
print f.read #=> こんにちは
f.close

詳細はIO のエンコーディングとエンコーディングの変換を参照してください。

Ruby/gtk4を使う場合、RubyでなくGTK 4、より正確にはGIOの入力関数を使うことがあります。 そのとき、エンコーディングが考慮されていないので、Rubyとしてはバイナリ入力のASCII-8BITでエンコーディングを設定することがあります。 そのときには必要なエンコーディングをforce_encodingメソッドで入力文字列に与えることが必要になります。

テキストの書き込み

テキストの書き込みは読み込みよりも単純です。

s = "あいうえお" # UTF-8
f = File.open("gr.txt", "w")
f.write(s) # UTF-8で出力
f.close

f = File.open("gr_euc.txt", "w")
f.set_encoding("EUC-JP")
f.write(s) # EUC-JPで出力
f.close

f = File.open("gr_sjis.txt", "w")
f.set_encoding("SJIS","UTF-8")
f.write(s) # Shift-JISで出力
f.close
標準入出力

ここでは、デフォルトの標準入出力である、キーボード入力と画面出力について扱います。 これらは、オペレーティング・システムによって、どのエンコーディングを使うかが決められます。 UBUNTUなどのLinuxオペレーティング・システムでは現在はほとんどUTF-8です。

ですから、Rubyスクリプトが他のエンコーディングの文字列を持っていて、それを画面出力するときにはUTF-8に変換しなければなりません。 この方法には2つあります。

  • 文字列のエンコーディングUTF-8に変換しておく。これはencodeメソッドでできます。
  • $stdoutの外部エンコーディングはデフォルトでnilになっている(つまり出力時に何の変換もしない)。それをUTF-8に設定すると出力時に自動的に変換をしてくれる。
$stdout.set_encoding(Encoding::UTF_8)

徒然Ruby(40)スレッドが意外に遅い

Fiberを書いたときから、次はスレッドを書こうと思っていましたが、時間がかかってしまいました。 その理由は、期待したとおりのスレッドの効果がなかったためです。 今回はそのことを書きますが、これはRubyのスレッドの抱えている問題なのか、自分のやり方が悪いのかははっきりしていません。

スレッドの基本

スレッドとは

  • 並行して走るプログラムで、Rubyの場合は「プログラム」はブロックになる
  • ひとつのプロセス内で複数のメソッドが並行して動き、プロセスをまたいでメソッドが動くことはない
  • Rubyの場合は一度にはひとつのメソッドしか動かない(例外有り)。複数のメソッドが交代で動くイメージ

Rubyには子プロセスを立ち上げる機能もあります。 それを使うと2つのプログラムが同時に動くことができます。 現在は複数コアのCPUがほとんどなので、まさに同時です。 それぞれのプログラムが関連することなく切り分けられれば、複数プロセスが最速になります。

Threadクラス

RubyではThreadクラスでスレッドを生成します。 次の例では最初のスレッドがaからzまでを画面に出力、2番めのスレッドが100から200までを画面に出力します。 スレッドは途中で切り替わるので、アルファベットと数字が混在して出力されます。

def ax100200
  t1 = Thread.new {("a".."z").each {|c| print "#{c}\n"}}
  t2 = Thread.new {(100..200).each {|x| print "#{x}\n"}}
  t1.join
  t2.join
end
  • Thread.newのブロックがひとつのスレッドになる。newメソッドの返り値はスレッドオブジェクトになる
  • このプログラムにはスレッドが3つあり、t1、t2とメインスレッド(最初に動くプログラム自身)がある
  • メインスレッドが終了すると、子孫メソッドも強制的に終了させられる。 それを避けるには、メインスレッドを無限ループにするか、joinメソッドで子孫メソッドの終了を待つようにする。 上記のプログラムでは、t1.joinでt1の終了までメインプログラムが待つようになり、t1の終了で再開の後にt2.joinでt2の終了を待つようになる

joinメソッドのタイミングは重要で、仮にt2生成の前にt1.joinしてしまうと、t1の終了後にt2が生成されることになり、並行には動いていないことになります。

スレッドが有効なケース

Rubyのスレッドは一度にはひとつのスレッドしか動かないので、CPUで大量の計算をするようなプログラムをスレッドにしても時間短縮にはなりません。 しかし、CPUに待ち時間があり、その間他のスレッドを実行することにより、全体の実行時間を短縮することは期待できます。

  • I/OはCPUに比べ低速なので、I/O待ちのあるプログラムに使う
  • 同様に通信もCPUに比べて低速なので、通信待ちのあるプログラムに使う。例えばダウンロードを別プロセスにするなど

これ以外に、同時に2つのものが動くような事象をプログラム化するときにはスレッドが向いています。 例えば点Aを点Bが最短で追跡するとき、Aの動きとBの動きをシミュレートするなどが考えられます。 ただ、スレッドの切り替わりをスレッド自身がコントロールできないので、シミュレーションは完全なものにはなりませんが。 そのモデルによりますが、ファイバーのほうが良い場合もありえます。

以上の考察に基づき、プログラムを試してみました。 その結果ははたして・・・・?

ファイルの読み込み

ファイルの読み込みには時間がかかるから、マルチスレッドにすれば速いのではないか? 実際にやってみました。

def s_read(files)
  files.each {|f| File.read(f)}
end

def c_read(files)
  threads = []
  files.each do |file|
    threads << Thread.new(file) {|f| File.read(f)}
  end
  threads.each {|t| t.join}
end

def s_or_c_input
  files = Dir.glob("_example/*.rb")

  t1 = Time.now
  s_read(files)
  t2 = Time.now
  p t2 - t1

  t1 = Time.now
  c_read(files)
  t2 = Time.now
  p t2 - t1
end

s_or_c_input

s_readがスレッドなしのシーケンシャル(一列に並んだ)に読み込むメソッド、c_readがコンカレント(同時並行)な読み込みのプログラムです。 実行してみると

$ ruby _example/example40.rb
0.011881702
0.034613109
$ ruby _example/example40.rb
0.000459287
0.034651356

なんと、シーケンシャルの方が速い。 しかも2回めは圧倒的な差に広がっています。

ということは、メソッドの生成にかかる時間が大きく影響しているのではないでしょうか。 また、2回目で大差になったのは読み込みにおけるキャッシュの効果ではないかと思いました。

書き込みではどうかと思い、実験しましたが、そちらもシーケンシャルの方が速かったです。 2回行うと、2回めの方が差が開きました。 書き込みにおいてもキャッシュの効果が出たようです。

コマンドの受付をスムーズに行う

コマンドを受け付けて、それに対応する処理をする場合、処理時間が長いと次の受付までの待ち時間が発生します。 それをスレッドを使うことによって待たずに済むようにすることができます。

Readlineクラスを使ってやってみました。

require "readline"

def rl
  threads = []
  # If the input is EOF (ctrl-d), Readline.readline returns nil.
  while buf = Readline.readline("> ", false)
    i = buf.to_i
    if 1 <= i && i <= 9
      threads << Thread.new(i) do |n|
        x = (1..(n*100000000)).inject {|a,b| a+b}
        File.open("tempfile","a") {|file| file.print("#{x}\n")}
      end
    end
  end
  threads.each {|t| t.join}
end

rl

目論見通り処理を待たずに次のプロンプトが出るのですが、マルチスレッドの影響でreadlineのプロンプトに乱れが出ました。 ちかちかしたり、プロンプトが2個でたりします。 readlineはスレッド対応しているとのことなので、原因は良くわかりませんでした。 終了させるにはCtrl-Dを押します(Linuxの場合)。 それはreadlineにはEOF(end-of-file)となって伝わり、readlineメソッドがnilを返してループを抜けることができます。 しかし、子メソッドの終了を待つので、プログラム全体の終了には時間がかかります。 これは高速化とはいえません。

結論

Rubyのスレッドは時間がかかるので、効果があるようなケースを見つけて使うことになると思います。 おそらくサーバ関係のプログラムでは効果を発揮すると思います。 また、ダウンロードやバックアップをバックグラウンドでやるのも効果がありそうです。 普段のちょっとしたプログラムでは使いそうもないな、というのが実感でした。