2022/06/02 今回の気になった bugs.ruby のチケット

今週は除算の切り上げを行う Numeric#ceildiv を追加する提案がありました。

[Feature #18809] Add Numeric#ceildiv

  • 「除算の切り上げ」を実現する Numeric#ceildiv を追加する提案
    • 「除算の切り上げ」とは、最も近い整数に切り上げられる除算の商を取得すること
class Integer
  # notice that b > 0 is assumed
  def ceildiv(b)
    (self + b - 1) / b
  end
end

# 例えば123アイテムあります。各ページに10個のアイテムを表示すると、何ページありますか?
p 123.ceildiv(10) # => 13
  • これはあると便利かも

2022/05/26 今回の気になった bugs.ruby のチケット

今週は Refinements で protected されたメソッドが呼び出せないバグ報告がありました。

[Bug #18806] protected methods defined by refinements can't be called

  • Refinements で定義された protected がメソッドが呼び出せないというバグ報告
class A
end

module MyRefine
  refine A do
    private def private_foo
      "refined"
    end

    def private_foo_in_refinement
      private_foo
    end

    protected def protected_foo
      "refined"
    end

    def protected_foo_in_refinement
      # この呼び出しがエラーになる
      protected_foo
    end
  end
end

class A
  using MyRefine

  def call_private
    private_foo
  end

  def call_private_through_refinement
    private_foo_in_refinement
  end

  def call_protected
    # この呼び出しがエラーになる
    protected_foo
  end

  def call_protected_through_refinement
    protected_foo_in_refinement
  end

  def is_defined
    # defined? だとメソッドが定義されているように振る舞う
    defined?(protected_foo)
  end
end

pp A.new.call_private
# => :refined

pp A.new.call_private_through_refinement
# => :refined

pp A.new.call_protected
# => NoMethodError: protected method `protected_foo' called for #<A:0x00007f23f35e9390>

pp A.new.call_protected_through_refinement
# => NoMethodError: protected method `protected_foo' called for #<A:0x00007f23f35e9390>

pp A.new.is_defined
# "method"
  • 久々に Refinement のバグみたな?
    • これ、今まで見つかってなかったんだ…
  • protected でも問題なく protected のメソッドとして呼び出せるのが期待する挙動な気がする

[Bug #18793] Select and Find behave differently for hashes

  • 以下のように Hash#selectHash#find で挙動が違うというバグ報告
# キーにマッチした結果が返ってくる
{ 1..10 => :a, 11 .. 20 => :b }.select { _1 === 12 }
# => {11..20=>:b}

# しかし find の場合は見つからない
{ 1..10 => :a, 11 .. 20 => :b }.find { _1 === 12 }
# => nil

# select の _1 はキーを受け取る
{ 1..10 => :a, 11 .. 20 => :b }.select { p _1 }
# => 1..10
#    11..20

# find は [キー, 要素] の配列を受け取る
{ 1..10 => :a, 11 .. 20 => :b }.find { p _1 }
# => [1..10, :a]
  • _1 ではなくて { |k,| } のように , を付けてブロックの引数を受け取ると配列の第一要素のみを受け取るので両方共同じ挙動になる
# キーにマッチした結果が返ってくる
p({ 1..10 => :a, 11 .. 20 => :b }.select { |k,| k === 12 })
# => {11..20=>:b}

# しかし find の場合は見つからない
p({ 1..10 => :a, 11 .. 20 => :b }.find { |k,| k === 12 })
# => [11..20, :b]

# select の _1 はキーを受け取る
{ 1..10 => :a, 11 .. 20 => :b }.select { |k,| p k }
# => 1..10
#    11..20

# find は [キー, 要素] の配列を受け取る
{ 1..10 => :a, 11 .. 20 => :b }.find { |k,| p k }
# => 1..10

[Bug #18771] IO.foreach/.readlines ignores the 4th positional argument

  • IO.readlines の引数シグネチャは以下のようになっている
readlines(name, sep, limit [, getline_args, open_args]) → array
  • 位置引数を3つ受け取って、残りはキーワード引数で受け取るが、位置引数を4つ渡してもエラーにならないというバグ報告
    • ArgumentError になるのが期待する挙動
File.readlines('file.txt', "\n", 10)
# => ["abc\n", "\n", "def\n"]
File.readlines('file.txt', "\n", 10, {})
# => ["abc\n", "\n", "def\n"]
File.readlines('file.txt', "\n", 10, {chomp: true})
# => ["abc\n", "\n", "def\n"]
File.readlines('file.txt', "\n", 10, false)
# => ["abc\n", "\n", "def\n"]
File.readlines('file.txt', "\n", 10, nil)
# => ["abc\n", "\n", "def\n"]
readlines(path, rs = $/, chomp: false, opts={}) -> [String]
readlines(path, limit, chomp: false, opts={}) -> [String]
readlines(path, rs, limit, chomp: false, opts={}) -> [String]
readlines(name, sep=$/ [, getline_args, open_args]) → array
readlines(name, limit [, getline_args, open_args]) → array
readlines(name, sep, limit [, getline_args, open_args]) → array
readlines(name, sep=$/ [, getline_args, open_args]) → array
readlines(name, limit [, getline_args, open_args]) → array
readlines(name, sep, limit [, getline_args, open_args]) → array

[Bug #18797] Third argument to Regexp.new is a bit broken

# OK
Regexp.new('abc', Regexp::NOENCODING)

# error: /.../n has a non escaped non ASCII character in non ASCII-8BIT script: /あああ/ (RegexpError)
Regexp.new('あああ', Regexp::NOENCODING)
re = Regexp.new('あああ', nil, 'n') # => /あああ/
pp re.options.anybits? Regexp::NOENCODING   # => true

pp re.encoding   # => #<Encoding:ASCII-8BIT>
pp re.source.encoding   # => #<Encoding:UTF-8>

# error: incompatible encoding regexp match (ASCII-8BIT regexp with UTF-8 string) (Encoding::CompatibilityError)
pp re =~ "あああ"
  • Regexp.new の引数でどう制御できるのか軽く調べてみたけどあんまりよくわからなかった…

2022/05/20 今回の気になった bugs.ruby のチケット

今週はパターンマッチの #deconstruct を拡張する提案がありました。

[Feature #18788] Support passing Regexp options as String to Regexp.new

  • Regexp.new の第二引数にオプションを渡すことができる
Regexp.new('foo', Regexp::IGNORECASE | Regexp::MULTILINE | Regexp::EXTENDED) # => /foo/imx
  • これを文字列を渡せるようにする提案
Regexp.new('foo', 'i')   # => /foo/i
Regexp.new('foo', :i)    # => /foo/i

Regexp.new('foo', 'imx') # => /foo/imx
Regexp.new('foo', :imx)  # => /foo/imx
Regexp.new('foo', Regexp::IGNORECASE)   # => /foo/i

# これも i になる
Regexp.new('foo', "hoge")   # => /foo/i

# i を文字列で渡せるように見えるが
Regexp.new('foo', "i")   # => /foo/i

# これも i になる
Regexp.new('foo', "m")   # => /foo/i

[Feature #14602] Version of dig that raises error if a key is not present

  • 以下のように要素が見つからなかった場合に例外が発生する #dig! を追加する提案
hash = {
    :name => {
        :first => "Ariel",
        :last => "Caplan"
    }
}

hash.dig!(:name, :first) # => Ariel
hash.dig!(:name, :middle) # raises KeyError (key not found: :middle)
hash.dig!(:name, :first, :foo) # raises TypeError (String does not have #dig! method)
  • #[] でも似たような事は実現できるがより自明にしたいのが目的
hash = {
    :name => {
        :first => "Ariel",
        :last => "Caplan"
    }
}

hash[:name][:first] # => Ariel
hash[:name][:middle] # => nil
hash[:name][:first][:foo] # => `[]': no implicit conversion of Symbol into Integer (TypeError)

[Feature #18774] Add Queue#pop(timeout:)

[Feature #18773] deconstruct to receive a range

  • #deconstruct_keys を定義する事でパターンマッチで { a:, b: } パターンを任意のオブジェクトで使うことができる
    • 引数に Hash のキーを受け取る事ができる
class Time
  # Hash パターンのキーを受け取る
  # in { year:, day: } なら [:year, :day]
  def deconstruct_keys(keys)
    # キーを元にしてパターンマッチに必要な Hash を返す
    keys.to_h { [_1, send(_1)] }
  end
end

time = Time.new(2020, 1, 1)
pp time

# time.year と time.day を受け取る事ができる
case time
in { year:, day: }
  pp year
  pp day
end
  • 同様に [a, b] の場合は #deconstruct で拡張できる
  • この引数に [] の個数を Range で受け取る提案
class DeconstructWithRange
  def initialize(values)
    @values = values
  end

  # range で in の配列の個数を 個数..個数 で受け取る
  # * がふくまれている場合は 個数..無限 になる
  def deconstruct(range)
    range.cover?(@values.length) ? @values : []
  end
end

case DeconstructWithRange.new([1, 2])
# deconstruct(2..2) を呼び出す
in ["hoge", "foo"]
  true
# deconstruct(3..3) を呼び出す
in ["hoge", "foo", "bar"]
  true
# deconstruct(2..) を呼び出す
in ["hoge", "foo", "bar", *]
  true
else
  true
end
class ActiveRecord::Relation
  def deconstruct(range)
    # 配列の個数が一致している時のみ record を読み込んでくる
    (loaded? || range.cover?(count)) ? records : nil
  end
end

case Person.all
in []
  "No records"
# Person.all のレコード数が1個の時のみレコードを読み込んで処理する
in [person]
  "Only #{person.name}"
else
  "Multiple people"
end
  • あれば便利そうな気がするけどわざわざ Range で受け取らなくてもよい気がするなあ
    • 個数 + * があるかどうか、の2つの情報を受け取るほうが意図は伝わりやすそう?
class ActiveRecord::Relation
  # 個数と * があるかどうかを受け取る
  def deconstruct(count, rest:)
    # 配列の個数が一致している時のみ record を読み込んでくる
    (loaded? || self.count == count) ? records : nil
  end
end

2022/05/12 今回の気になった bugs.ruby のチケット

[Bug #18768] Inconsistent behavior of IO, StringIO and String each_line methods when return paragraph and chomp: true passed

"a\n\nb\n\nc\n".each_line("", chomp: true).to_a
#=> ["a\n", "b\n", "c\n"]

StringIO.new("a\n\nb\n\nc\n").each_line("", chomp: true).to_a
#=> ["a\n", "b\n", "c"]

File.open('chomp.txt').each_line("", chomp: true).to_a
#=> ["a", "b", "c\n"]
  • chomp.txt の中身
File.read('chomp.txt')
#=> "a\n\nb\n\nc\n"
  • String#each_line の挙動は以下の通り
# \n で区切りつつ末尾の \n を取り除く
p "a\n\nb\n\nc\n".each_line(chomp: true).to_a
#=> ["a", "", "b", "", "c"]

# `\n\n` 区切りで分割する
p "a\n\nb\n\nc\n".each_line("").to_a
#=> ["a\n\n", "b\n\n", "c\n"]
  • これはどれが期待する挙動になるんですかね…

いまさら聞けない!波ダッシュと全角チルダ問題についてまとめてみた

元々のきっかけは以下のように RubyUTF-8 の波文字を SJIS に変換しようとしたらエラーになってしまいました。

# UTF-8 の 〜 文字を SJIS に変換するとエラーになる
# error: `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError)
"".encode("SJIS")

これの原因を調べてみたら 波ダッシュと全角チルダ問題 にたどり着いたのでその問題と歴史についてまとめてみました。

諸注意

波ダッシュと全角チルダ問題 に関しては一般的な話なんですが、変換ルールやエンコーディングの指定の仕方などは CRuby での話になります。 他の言語や処理系だと結果が変わるかもしれないので注意してください。 また、この記事でのソースコードはすべて UTF-8 になります。
それでは本題に入っていきましょう。

RubyUTF-8 の波文字()を SJIS に変換するとエラーになる

冒頭にも記述したように元々この問題を調べようとしたきっかけは次のように RubyUTF-8 の波文字()を SJIS に変換しようとするとエラーになってしまうことでした。

# UTF-8 の 〜 文字を SJIS に変換するとエラーになる
# error: `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError)
"".encode("SJIS")

上記のコードで変換しようとした波文字()は Unicode のコードポイントだと U+301C になるんですが、その文字が SJIS への変換に失敗 しています(ちなみに Ruby では SJISWindows-31Jエイリアスとして定義されています)。

# Ruby で Unicode のコードポイントを表示する
pp "".unpack1("U*").to_s(16)
# => "301c"

SJIS にも波文字自体は存在しているのに不思議ですよね? そもそも波文字とはどういう文字なのでしょうか。 Unicode の波文字について詳しくみてみましょう。

波文字は2つあった!

実は Unicode には波文字を表す文字が2つ存在しています。 それが『波ダッシュ文字(U+301C)』と『全角チルダ文字(U+FF5E)』になります。
最初に Ruby のコードで記述していた波文字は 波ダッシュ文字(U+301C) になります。

文字 意味 Unicode のコードポイント UTF-8 のバイト列
波ダッシュ(WAVE DASH) U+301C \xE3809C
全角チルダ(FULLWIDTH TILDE) U+FF5E \xEFBD9E

この2つの文字は見た目上は似た文字として表示されていると思うのですが、実際には全く異なる文字になります。 試しにここに書いてある文字をバイナリエディタにコピペしてみたり Ruby でコードポイントを取得してみると全然違う文字だと言うことがわかると思います。

# Unicode のコードポイントを出力してみる
# これは波ダッシュ
pp "".unpack1("U*").to_s(16)
# => "301c"

# これは全角チルダ
pp "".unpack1("U*").to_s(16)
# => "ff5e"

このように が波ダッシュ(U+301C) が全角チルダ(U+FF5E)全く異なる文字になります。
この『Unicode には波文字が2種類存在する事』が SJIS に変換できない問題と深く関わってきます。

全角チルダ (U+FF5E) は SJIS に変換できる

先程紹介した Unicode の2つの波文字なのですが 波ダッシュ (U+301C)SJIS に変換するとエラーになることはわかりました。

# これは波ダッシュ
nami = ""
pp nami.unpack1("U*").to_s(16)
# => "ff5e"

# SJIS に変換するとエラーになる
pp nami.encode("SJIS")
# => "\x{8160}"

しかし、実は 全角チルダ (U+FF5E)SJIS に変換する事ができます。

# これは全角チルダ
tilde = ""

pp tilde.unpack1("U*").to_s(16)
# => "ff5e"

# SJIS に変換する事ができる!
pp tilde.encode("SJIS")
# => "\x{8160}"

これは一体どういうことなのでしょうか。

波ダッシュ (U+301C) を Shift_JIS に変換すると…

ここでは変換後のエンコーディングの話をします。
Ruby では SJIS と名前が似ている Shift_JIS というエンコーディングも存在しています。 実は SJIS ではなくて Shift_JIS であれば UTF-8波ダッシュ (U+301C)Shift_JIS に変換することができます。

# これは波ダッシュ
nami = ""

pp nami.unpack1("U*").to_s(16)
# => "301c"

# Shift_JIS に変換する事ができる!
pp nami.encode("Shift_JIS")
# => "\x{8160}"

また逆に UTF-8全角チルダ (U+FF5E)Shift_JIS に変換すると エラー になります。

# これは全角チルダ
tilde = ""

pp tilde.unpack1("U*").to_s(16)
# => "ff5e"

# SJIS に変換するとエラーになる
# error: `encode': U+FF5E from UTF-8 to Shift_JIS (Encoding::UndefinedConversionError)
pp tilde.encode("Shift_JIS")

Ruby の変換ルールとしては以下のようになっています。

変換前の UTF-8 の文字 SJIS Shift_JIS
波ダッシュ (U+301C) 変換できない 変換できる
全角チルダ (U+FF5E) 変換できる 変換できない

次は SJISShift_JIS の違いについて調べてみましょう。

Ruby における SJISShift_JIS の違い

ここでは Ruby における SJISShift_JIS の違いについて簡単に解説します。
まず Shift_JIS はその名の通り文字コードShift_JIS のことを指しています。 Shift_JIS とは日本語を含む文字列を表現するために用いられる文字コードになります。 また Shift_JISJIS X 0208 として標準化されています。

Ruby ではこの文字コードShift_JIS として利用できます。

次に SJIS なのですが Rubyエンコーディングにおいて SJISCP932 Windows-31J と同じエンコーディングとして定義されています。 Windows-31J とは MicrosoftShift_JIS を独自拡張したエンコーディングになります。
例えば という文字は Shift_JIS には存在しませんが Windows-31J には存在する文字になります。

# Windows-31J に変換可能
pp "".encode("Windows-31J")   # => "\x{8740}"

# SJIS や CP932 も同様
pp "".encode("SJIS")   # => "\x{8740}"
pp "".encode("CP932")   # => "\x{8740}"

# Shift_JIS には変換できない
# error: U+2460 from UTF-8 to Shift_JIS (Encoding::UndefinedConversionError)
pp "".encode("Shift_JIS")

なので厳密に言うと SJISShift_JIS異なるエンコーディング になります。

まとめると Ruby において SJISShift_JIS は以下のような関係になっています。

より詳しい違いが知りたい人は以下を参照してください。

以降は SJIS という表記だとややこしいので CP932 と表記し『 CP932Shift_JIS 』について解説していきます。

JIS X 0208 の波文字とは

ここでは Shift_JISCP932 の規格である JIS X 0208 の話をします。 実は JIS X 0208 で定義されている波文字は Unicode とは違って『波ダッシュ文字(\x8160)だけ』が存在しています。 (JIS X 0208 を更に拡張した JIS X 0213 には全角チルダも定義されているんですがここでは一旦置いておきます。
エンコーディングの波文字の情報は以下のようになります。

文字 Unicode のコードポイント UTF-8 のバイト列 JIS X 0208 のバイト列
波ダッシュ(WAVE DASH) U+301C \xE3809C \x8160
全角チルダ(FULLWIDTH TILDE) U+FF5E \xEFBD9E ない

なので UTF-8波ダッシュShift_JIS に変換すると Shift_JIS波ダッシュに変換されるのは期待する挙動と言えます。

# 波ダッシュを Shift_JIS に変換すると Shift_JIS の波ダッシュになる
p "\u301C".encode("Shift_JIS")   # => "\x{8160}"

では、なぜ UTF-8 の波文字を CP932 に変換するときに『対応する文字がある波ダッシュがエラー』になって『対応する文字がない全角チルダがエラー』になるのでしょうか。

# 全角チルダを CP932 に変換するとなぜか JIS X 0208 の波ダッシュになる
p "\uFF5E".encode("CP932")     # => "\x{8160}"

# 波ダッシュを CP932 に変換するとJIS X 0208 の波ダッシュは存在するのにエラーになる
# error: `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError)
p "\u301C".encode("CP932")   # => "\x{8160}"

この疑問を解消するためにまず JIS X 0208波ダッシュ、つまり『 CP932Shift_JIS から UTF-8 に変換するとどうなるのか』を試してみましょう。

JIS X 0208波ダッシュUTF-8 に変換するとどうなる?

今度は逆に JIS X 0208波ダッシュUTF-8 に変換するとどうなるのか見てみましょう。 実際に RubyShift_JISCP932 から波ダッシュUTF-8 に変換してみます。
Ruby で変換する場合は JIS X 0208波ダッシュ文字をバイト列で定義し String#encode に変換元となるエンコーディングの情報として CP932Shift_JIS をそれぞれ指定して UTF_8 に変換してみます。

# CP932 でも Shift_JIS でもバイト列は同じ
jis_nami = "\x81\x60"

# CP932 -> UTF_8 に変換すると全角チルダになる
pp jis_nami.encode("UTF-8", "CP932").unpack1("U*").to_s(16)
# => "ff5e"

# Shift_JIS -> UTF_8 に変換すると波ダッシュになる
pp jis_nami.encode("UTF-8", "Shift_JIS").unpack1("U*").to_s(16)
# => "301c"

このように Shift_JIS -> UTF_8 は『同じ文字への変換』になっています。 しかし CP932 -> UTF_8 は『異なる文字への変換』になっていることがわかります。

文字コード 変換前 UTF-8 へ変換
CP932 波ダッシュ 全角チルダ
Shift_JIS 波ダッシュ 波ダッシュ

この『 CP932波ダッシュUTF-8 に変換したときに誤った文字になってしまうこと』がいわゆる 波ダッシュ問題 として一般的に扱われている問題になります。
では、なぜこのような変換になってしまったのでしょうか。 Unicode の歴史を調べてみましょう。

Unicode波ダッシュの例示字形が間違っていた

結論からいうと Unicode 7.0 で定義されていた波ダッシュ例示字形 が間違った形で記載されてしまっていたのが要因になっているようです。
どういうことかと言うと現在の Unicode では波ダッシュも全角チルダ のように 上がって下がる ような字形になります。 これは JIS X 0208 も同様の字形になっています。
しかし Unicode 7.0 では波ダッシュが以下のように 下がって上がる という文字として例示字形が記載されていました。

文字コード 波ダッシュ 全角チルダ
Unicode 7.0
※これが間違い
Unicode 8.0
JIS X 0208

実際に Unicode のコードチャートにも以下のように記載されています。

Unicode 7.0

Unicode 8.0

WAVE DASH の例示字形が異なる形になっているのがわかると思います。 これが CP932 -> UTF-8 に変換した際に 波ダッシュ -> 全角チルダ となってしまっている要因になっています。
この Unicode 7.0Unicode 8.0波ダッシュの例示字形の問題に関しては以下の記事で詳しく解説されているので気になる人は読んでみるとよいと思います。

問題は逆だった

ここからは仮説になってくるのですが先程説明したように JIS X 0208波ダッシュUnicode 7.0波ダッシュでは字形が異なっていました。 なのでそのまま JIS X 0208波ダッシュUTF-8波ダッシュに変換してしまうと 文字としては同じ なのですが 見た目としては違う文字 に見えてしまいます。
この問題を回避するために Windows では JIS X 0208波ダッシュUTF-8 に変換する時に『 JIS X 0208波ダッシュと同じ字形であった全角チルダに変換する』というルールができたのではないかと考えられます。 これにより Windows で使われている CP932 は『波ダッシュ -> 全角チルダ』という変換になり、逆に JIS X 0208 と同等である Shift_JIS はそのままの文字である『波ダッシュ -> 波ダッシュ』になっているのではなかろうかと思います。

文字コード 変換前 UTF-8 へ変換
CP932 波ダッシュ 全角チルダ
Shift_JIS 波ダッシュ 波ダッシュ


逆に UTF-8 -> CP932 と変換するときも同様に『 全角チルダ -> 波ダッシュ 』となり UTF-8 -> Shift_JIS だと『 波ダッシュ -> 波ダッシュ 』になっているのだと予想しています。

文字コード 変換前 CP932 へ変換 Shift_JIS へ変換
UTF-8 波ダッシュ エラー 波ダッシュ
UTF-8 全角チルダ 波ダッシュ エラー

まとめ

文字 意味 Unicode のコードポイント UTF-8 のバイト列
波ダッシュ(WAVE DASH) U+301C \xE3809C
全角チルダ(FULLWIDTH TILDE) U+FF5E \xEFBD9E
変換前の文字コード 変換後の文字コード 変換前の文字 変換後の文字
UTF-8 CP932 波ダッシュ(U+301C) エラー
UTF-8 CP932 全角チルダ(U+FF5E) 波ダッシュ(\x8160)
UTF-8 Shift_JIS 波ダッシュ(U+301C) 波ダッシュ(\x8160)
UTF-8 Shift_JIS 全角チルダ(U+FF5E) エラー
CP932 UTF-8 波ダッシュ(\x8160) 全角チルダ(U+FF5E)
Shift_JIS UTF-8 波ダッシュ(\x8160) 波ダッシュ(U+301C)

参照

余談:RubyUnicode波ダッシュSJIS波ダッシュに変換する

以下のように #encodefallback オプションで制御する事が可能です。

pp "".encode("SJIS", fallback: { "\u301C" => "\x81\x60".force_encoding("SJIS") })

別解としては #tr波ダッシュを全角チルダに変換してから #encode するやり方も考えられます。

pp "".tr("\u301C", "\uFF5E").encode("SJIS")

余談: iconv の変換ルール

iconv だと次のような変換ルールとなっています。

変換前の文字コード 変換後の文字コード 変換前の文字 変換後の文字
UTF-8 CP932 波ダッシュ(U+301C) 波ダッシュ(U+301C) ※これが Ruby と異なる
UTF-8 CP932 全角チルダ(U+FF5E) 波ダッシュ(\x8160)
UTF-8 Shift_JIS 波ダッシュ(U+301C) 波ダッシュ(\x8160)
UTF-8 Shift_JIS 全角チルダ(U+FF5E) エラー
CP932 UTF-8 波ダッシュ(\x8160) 全角チルダ(\xEFBD9E)
Shift_JIS UTF-8 波ダッシュ(\x8160) 波ダッシュ(\xE3809C)

以下、実行ログ。

# utf-8 の波ダッシュ -> cp932 は変換できる
$ echo -n "〜" | iconv -f utf-8 -t cp932 | od -tx1 -An
 81 60

# utf-8 の全角チルダ -> cp932 は変換できる
$ echo -n "~" | iconv -f utf-8 -t cp932 | od -tx1 -An
 81 60
# utf-8 の波ダッシュ -> shift_jis は変換できる
$ echo -n "〜" | iconv -f utf-8 -t shift_jis | od -tx1 -An
 81 60

# utf-8 の全角チルダ -> shift_jis は変換できない
$ echo -n "~" | iconv -f utf-8 -t shift_jis | od -tx1 -An
iconv: 位置 0 に不正な入力シーケンスがあります
# cp932 -> utf-8 は全角チルダ
$ echo -n $'\x81\x60' | iconv -f cp932 -t utf-8 | od -tx1 -An
 ef bd 9e

# shift_jis -> utf-8 は波ダッシュ
$ echo -n $'\x81\x60' | iconv -f shift_jis -t utf-8 | od -tx1 -An
 e3 80 9c

UTF-8 -> CP932 の変換だけ Ruby と異なっています。

2022/05/05 今回の気になった bugs.ruby のチケット

今週は Array#undigits の提案がありました。

[Feature #18762] Add an Array#undigits that compliments Integer#digits

class Array
  def undigits(base = 10)
    each_with_index.sum do |digit, exponent|
      digit * base**exponent
    end
  end
end

pp 42.digits
# => [2, 4]

pp 42.digits.undigits
#=> 42

# 16進数で変換
pp 42.digits(16)
# => [10, 2]

pp 42.digits(16).undigits(16)
#=> 42
  • より厳密に提示されている Ruby の実装コード
class Array
  def undigits(base = 10)
    base_int = base.to_int
    raise TypeError, "wrong argument type #{base_int.class} (expected Integer)" unless base_int.is_a?(Integer)
    raise ArgumentError, 'negative radix' if base_int.negative?
    raise ArgumentError, "invalid radix #{base_int}" if base_int < 2

    each_with_index.sum do |digit, exponent|
      raise MathDomainError, 'out of domain' if digit.negative?

      reverse.reduce(0) do |acc, digit|
        acc * base + digit
      end
    end
  end
end
  • あると便利なんですかね

2022/04/29 今回の気になった bugs.ruby のチケット

今週は右代入を TracePoint でフックする場合のバグ報告がありました。

[Bug #18753] lineno= is not returning an integer

  • ARGF.send(:lineno=, 1) の戻り値が nil になっているというバグ報告
# これは 1 を返す
p (ARGF.lineno=1)
# => 1

# これは nil を返す
p ARGF.send(:lineno=, 1)
# => nil
  • この問題は最新版では修正済み

[Bug #18752] defined? String.instance_method(:abcdefg) will return a "method" string instead nil.

  • defined? String.instance_method(:abcdefg) した時に nil を返すことを期待するが "method" が返るというバグ報告
p defined? String.instance_method(:abcdefg)
# => nil

p defined? String.instance_method(:abcdefg111111111111)
# => nil
  • これは defined?String.instance_method の呼び出しに対してチェックしているので期待する挙動になる
  • 逆に定義されてないメソッドを呼び出そうとすると nil を返す
p defined? String.abcdefg111111111111
# => nil

[Bug #18629] block args array splatting assigns to higher scope _ var

  • 以下のように仮引数が _ でない場合はスコープの外の変数は書き換わらないが _ の場合は外のスコープの変数が書き換わってしまうというバグ報告
# これは書き換わらない
v = 1; [[2]].each{ |(v)| }; p v   # => 1

# これは書き換わってしまう
_ = 1; [[2]].each{ |(_)| }; p _   # => 2
  • これは最新版で修正済み

Bug #18740 Use of rightward assignment changes line number needed for line-targeted TracePoint(https://bugs.ruby-lang.org/issues/18740)

  • 次のように右代入を使用している際に TracePoint でフックできないというバグ報告
def foo
  (1..)
      .lazy
      .filter { _1.even? }
      .take(10)
      .to_a => result

  puts result
end

# foo メソッドの 2行目の式に対して TracePoint をフックする
# error: `enable': can not enable any hooks (ArgumentError)
TracePoint.new(:line) { puts 'Hi' }.enable(target: RubyVM::InstructionSequence.of(method :foo), target_line: 2)
foo
  • 右代入ではなくて左代入だと問題なく動作する
def foo
  result = (1..)
      .lazy
      .filter { _1.even? }
      .take(10)
      .to_a

  puts result
end

# foo メソッドの 2行目の式に対して TracePoint をフックする
# OK
TracePoint.new(:line) { puts 'Hi' }.enable(target: RubyVM::InstructionSequence.of(method :foo), target_line: 2)
foo
def foo
  (1..)
      .lazy
      .filter { _1.even? }
      .take(10)
      .to_a => result   # <- この行数を指定すると OK

  puts result
end

# foo メソッドの 2行目の式に対して TracePoint をフックする
# OK
TracePoint.new(:line) { puts 'Hi' }.enable(target: RubyVM::InstructionSequence.of(method :foo), target_line: 6)
foo
  • 知らなかったので知見
  • これがバグなのか仕様なのかは現時点では未定義ぽい