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#select
とHash#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]
- RDoc だと以下の通り
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
Regexp.new
の第三引数が壊れているというバグ報告- 第二引数との挙動が違っているらしい
Regexp.new
の第二引数にRegexp::NOENCODING
を渡すとn
オプションと同じ挙動になる- 正規表現のマッチ時に文字列のエンコーディングを無視し、バイト列としてマッチする
- Regexp::NOENCODING (Ruby 3.1 リファレンスマニュアル)
- このオプションの時に
ASCII
文字以外を渡すとエラーになるぽい?
# 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)
Regexp.new
の第三引数は正規表現のエンコーディングを制御する事ができる"n"
,"N"
が与えられた時には、生成された正規表現のエンコーディングはASCII-8BIT
になる- Regexp.compile (Ruby 3.1 リファレンスマニュアル)
- 期待する挙動としては
Regexp::NOENCODING
と同じっぽい?けど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
- 実際のユースケースは限られているがまったくないわけではない
- https://bugs.ruby-lang.org/issues/18788#note-3
- rubocop : https://github.com/rubocop/rubocop-ast/blob/816dfe7f2ca4e92c7eda226a9e8b44aa9fa81e81/lib/rubocop/ast/node_pattern/lexer.rb#L21-L58
- parser : https://github.com/whitequark/parser/blob/09d681e534885f1aa22f0099089841ae9d86f847/lib/parser/builders/default.rb#L2224-L2242
- ちなみに
Regexp::IGNORECASE
Regexp::MULTILINE
Regexp::EXTENDED
以外の値を渡すとRegexp::IGNORECASE
になる- これも混乱するので対応する理由として上げられている
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:)
Thread::Queue#pop
の引数にtimeout:
を追加する提案- 以前から提案はあったけどタイムアウトしたときにどうなるのかの議論で止まっていたらしい
- 例外を発生させるのか、
nil
を返すのか - https://bugs.ruby-lang.org/issues/18774#note-1
- 例外を発生させるのか、
Thread::Queue#pop(timeout:, exception: true/false/class)
のようにexception:
を追加するコメントもある
[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
- 以下のように
String#each_line
StringIO#each_line
File#each_line
に特定の引数を渡したときの挙動に一貫性がないというバグ報告"", chomp: true
を渡した時- String#each_line (Ruby 3.1 リファレンスマニュアル)
- StringIO#each (Ruby 3.1 リファレンスマニュアル)
- るりまには
chomp
の引数の記述がない…
- るりまには
- IO#each (Ruby 3.1 リファレンスマニュアル)
"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"]
- これはどれが期待する挙動になるんですかね…
いまさら聞けない!波ダッシュと全角チルダ問題についてまとめてみた
元々のきっかけは以下のように Ruby で UTF-8
の波文字を SJIS
に変換しようとしたらエラーになってしまいました。
# UTF-8 の 〜 文字を SJIS に変換するとエラーになる # error: `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError) "〜".encode("SJIS")
これの原因を調べてみたら 波ダッシュと全角チルダ問題 にたどり着いたのでその問題と歴史についてまとめてみました。
諸注意
波ダッシュと全角チルダ問題 に関しては一般的な話なんですが、変換ルールやエンコーディングの指定の仕方などは CRuby での話になります。
他の言語や処理系だと結果が変わるかもしれないので注意してください。
また、この記事でのソースコードはすべて UTF-8
になります。
それでは本題に入っていきましょう。
Ruby で UTF-8
の波文字(〜
)を SJIS
に変換するとエラーになる
冒頭にも記述したように元々この問題を調べようとしたきっかけは次のように Ruby で UTF-8
の波文字(〜
)を SJIS
に変換しようとするとエラーになってしまうことでした。
# UTF-8 の 〜 文字を SJIS に変換するとエラーになる # error: `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError) "〜".encode("SJIS")
上記のコードで変換しようとした波文字(〜
)は Unicode
のコードポイントだと U+301C
になるんですが、その文字が SJIS
への変換に失敗 しています(ちなみに Ruby では SJIS
が Windows-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 ) |
変換できる | 変換できない |
次は SJIS
と Shift_JIS
の違いについて調べてみましょう。
Ruby における SJIS
と Shift_JIS
の違い
ここでは Ruby における SJIS
と Shift_JIS
の違いについて簡単に解説します。
まず Shift_JIS
はその名の通り文字コードの Shift_JIS
のことを指しています。
Shift_JIS
とは日本語を含む文字列を表現するために用いられる文字コードになります。
また Shift_JIS
は JIS X 0208
として標準化されています。
Ruby ではこの文字コードが Shift_JIS
として利用できます。
次に SJIS
なのですが Ruby のエンコーディングにおいて SJIS
は CP932
Windows-31J
と同じエンコーディングとして定義されています。
Windows-31J
とは Microsoft が Shift_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")
なので厳密に言うと SJIS
と Shift_JIS
は 異なるエンコーディング になります。
まとめると Ruby において SJIS
と Shift_JIS
は以下のような関係になっています。
SJIS
とCP932
Windows-31J
の 3つは同じエンコーディングとして扱われているWindows-31J
(SJIS
CP932
) は Microsoft がShift_JIS
を独自拡張したエンコーディングになるShift_JIS
とWindows-31J
は異なるエンコーディングになるShift_JIS
もWindows-31J
もJIS X 0208
という規格に準じている
より詳しい違いが知りたい人は以下を参照してください。
- Shift_JIS と Windows-31J (MS932) の違いを整理してみよう |
- Python♪Windowsの「Shift JIS」の落とし穴 | Snow Tree in June
- Encoding::SHIFT_JIS (Ruby 3.1 リファレンスマニュアル)
- Encoding::CP932 (Ruby 3.1 リファレンスマニュアル)
以降は SJIS
という表記だとややこしいので CP932
と表記し『 CP932
と Shift_JIS
』について解説していきます。
JIS X 0208
の波文字とは
ここでは Shift_JIS
や CP932
の規格である 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
の波ダッシュ、つまり『 CP932
や Shift_JIS
から UTF-8
に変換するとどうなるのか』を試してみましょう。
JIS X 0208
の波ダッシュを UTF-8
に変換するとどうなる?
今度は逆に JIS X 0208
の波ダッシュを UTF-8
に変換するとどうなるのか見てみましょう。
実際に Ruby で Shift_JIS
や CP932
から波ダッシュを UTF-8
に変換してみます。
Ruby で変換する場合は JIS X 0208
の波ダッシュ文字をバイト列で定義し String#encode
に変換元となるエンコーディングの情報として CP932
と Shift_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.0
と Unicode 8.0
の波ダッシュの例示字形の問題に関しては以下の記事で詳しく解説されているので気になる人は読んでみるとよいと思います。
- UnicodeのWAVE DASH例示字形が、25年ぶりに修正された理由 - INTERNET Watch Watch
- ~波ダッシュ~って字形が変わった?|IT情報メディアサイト idearu(アイディアル)
問題は逆だった
ここからは仮説になってくるのですが先程説明したように 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
の波文字は 波ダッシュ文字(U+301C
) と 全角チルダ文字(U+FF5E
) の2種類ある
文字 | 意味 | Unicode のコードポイント | UTF-8 のバイト列 |
---|---|---|---|
〜 |
波ダッシュ(WAVE DASH) | U+301C |
\xE3809C |
~ |
全角チルダ(FULLWIDTH TILDE) | U+FF5E |
\xEFBD9E |
- Ruby では
SJIS
とShift_JIS
は異なるエンコーディングとして定義されているSJIS
はCP932
とWindows-31J
と同等のエンコーディングとして定義されている
- Ruby では以下のようなルールで変換される
変換前の文字コード | 変換後の文字コード | 変換前の文字 | 変換後の文字 |
---|---|---|---|
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 ) |
参照
余談:Ruby で Unicode の波ダッシュを SJIS の波ダッシュに変換する
以下のように #encode
の fallback
オプションで制御する事が可能です。
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
Integer#digits
と対になるArray#undigits
を追加する提案Integer#digits
は位取り記数法で表記した数値を配列を返す- Integer#digits (Ruby 3.1 リファレンスマニュアル)
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
- これは右代入の場合に指定する行数を2行目ではなくて6行目を指定する必要があるそうです
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
- 知らなかったので知見
- これがバグなのか仕様なのかは現時点では未定義ぽい