😸

【Ruby】Range#include?とRange#cover?の違い

2023/04/24に公開
5

Range#include? と Range#cover?の違い

前提

本記事の内容は'MRI'いわゆる'CRuby'を対象としています。
また、Rubyのバージョンは3.2です。

概要

Range#include? と Range#cover? は以下のように、どちらも引数が指定した範囲に含まれるかどうかを判定する時に使われています。

(1.1..2.3).include?(1.1)    # => true
(1.1..2.3).cover?(1.1)      # => true

しかし、その違いがよく分からなかったため、本記事ではその違いについて説明します。
本記事では、公式ドキュメントへの理解を深め、最終的にはCRubyのコードについても考察していきたいと思います。

結論

公式ドキュメントでは書かれていなかったことで、自分で調べたことを補足していきます。

Range#include?

公式ドキュメント

Range#include? (Ruby 3.2 リファレンスマニュアル)

obj が範囲内に含まれている時に true を返します。そうでない場合は、false を返します。
<=> メソッドによる演算により範囲内かどうかを判定するには Range#cover? を使用してください。
始端・終端・引数が数値であれば、 Range#cover? と同様の動きをします。

上記に対しての補足は、以下のとおりです。

2文字以上の文字範囲を比較する時、Range#include?はRange#eachを使用して、引数とRangeの各要素を==で比較することで、範囲内に要素が存在するかどうか判定します。
範囲の始点と終点が1文字の時で、引数が1文字なら始点および終点と引数を比較します。引数が1文字でないなら、問答無用でfalseになります。
また、始端・終端・引数が数値であれば、C言語では、cover?の処理の関数r_cover_pを呼び出しています。

ruby/range.c at v3_2_1 · ruby/ruby

Range#cover?

公式ドキュメント

Range#cover? (Ruby 3.2 リファレンスマニュアル)

obj が範囲内に含まれている時に true を返します。
Range#include? と異なり <=> メソッドによる演算により範囲内かどうかを判定します。 Range#include? は原則として離散値を扱い、 Range#cover? は連続値を扱います。(数値については、例外として Range#include? も連続的に扱います。)

上記に対しての補足は、以下のとおりです。

引数とRangeの始点および終点を<=>で比較して、範囲内にあるかを判定します。そのため、Range#include?よりも処理が早いです。

詳細

以降は、先の結論に至った理由を公式ドキュメント元に説明していきます。

Range#include? と Range#cover?の文字列比較について

Range#cover? (Ruby 3.2 リファレンスマニュアル)では以下のような文字列に対する、両者の比較があります。

('b'..'d').include?('ba')   # => false
('b'..'d').cover?('ba')     # => true

以下、この違いについて考察します。

文字列比較について

最初に文字列の比較方法について考察します。複数文字の文字列の比較に関しては以下の記事を参考にしました。

文字列の大小比較をもう少し詳しく調べてみる(チェリー本の補足として) - Qiita

こちらでは、文字比較を各バイトの文字コード(半角英数字ではASCIIコード)に対して、一要素ずつ比較していることが分かります。そのため、以下において'b'と'd'のバイトが比較され大小が決められています。

'ba' <=> 'd' # => -1

'ba'.bytes # => [98, 97]
'd'.bytes #=> [100]
'b'.bytes[0] <=> 'd'.bytes[0] # => -1

実際、ruby/string.c at v3_2_1 · ruby/rubyでは、同じ文字列の長さまではmemcmpで文字列を比較しています。また、同じ文字列が続いて長さが違う場合、長さの少ない方が小さいと判定されます。

rb_str_cmp(VALUE str1, VALUE str2)
{
    long len1, len2;
    const char *ptr1, *ptr2;
    int retval;

    if (str1 == str2) return 0;
    RSTRING_GETMEM(str1, ptr1, len1);
    RSTRING_GETMEM(str2, ptr2, len2);
    if (ptr1 == ptr2 || (retval = memcmp(ptr1, ptr2, lesser(len1, len2))) == 0) {
        if (len1 == len2) {
            /*略*/
            return 0;
        }
        if (len1 > len2) return 1;
        return -1;
    }
    if (retval > 0) return 1;
    return -1;
}

文字列比較におけるRange#include?とRange#cover?の動き

次に、二つのメソッドの内部の動きを見てみます。
内部の演算がわかりやすいように、以下のようにメソッドをオーバーライドします。

module Foo
  def <=>(other)
    puts "#{self} <=> #{other}"
    super
  end
  def ==(other)
    puts "#{self} == #{other}"
    super
  end
  def ===(other)
    puts "#{self} === #{other}"
    super
  end
  def equal?(other)
    puts "#{self} equal? #{other}"
    super
  end
  def eql?(other)
    puts "#{self} eql? #{other}"
    super
  end
  def hash
    puts "hash #{self}"
    super
  end
  def each(*)
    puts "#{self} each"
    super
  end
end

String.prepend(Foo)
# Range.prepend(Foo) => このケースでは、この行が無くても結果が変わらない

puts "include?('ba')?"
puts ('b'..'d').include?('ba')
puts "\ncover?('ba')"
puts ('b'..'d').cover?('ba') 

この結果が、以下のようになります。

include?('ba')?
false

cover?('ba')
b <=> ba
ba <=> d
true

これで、少なくともcover?が境界としか比較していないことが分かります。
一方、include?は、これだとどういう動作をしているか分からないため、Cコードの方を見てみます。

ruby/string.c at v3_2_1 · ruby/ruby

if (RSTRING_LEN(beg) == 1 && RSTRING_LEN(end) == 1) {
            if (RSTRING_LEN(val) == 0 || RSTRING_LEN(val) > 1)
                return Qfalse;
            else {
                char b = *bp;
                char e = *ep;
                char v = *vp;

                if (ISASCII(b) && ISASCII(e) && ISASCII(v)) {
                    if (b <= v && v < e) return Qtrue;
                    return RBOOL(!RTEST(exclusive) && v == e);
                }
            }
        }

上記の部分で分かる通り、対象範囲が1文字で引数が1文字でない時は問答無用でfalseになるみたいです。また、引数が1文字の時は、境界との比較をしています。
範囲が2文字以上の場合は、次章で説明します。

Range#include? と Range#cover?の日付の比較について

Range#cover? (Ruby 3.2 リファレンスマニュアル)では以下のような日付に対する、両者の比較があります。

require 'date'
(Date.today - 365 .. Date.today + 365).include?(DateTime.now)  #=> false
(Date.today - 365 .. Date.today + 365).cover?(DateTime.now)    #=> true

以下、この違いについて考察します。

日付の比較

そもそも、DateとDateTimeの比較は以下のようになります。

require 'date'
d = Date.new(2023, 4, 11)
puts d #=> #<Date: 2023-04-11 ((2460046j,0s,0n),+0s,2299161j)>

dt_00_00 = DateTime.new(2023, 4, 11, 0, 0)
puts dt_00_00 #=> #<DateTime: 2023-04-11T00:00:00+00:00 ((2460046j,0s,0n),+0s,2299161j)>

dt_00_01 = DateTime.new(2023, 4, 11, 0, 1)
puts dt_00_01 #=> #<DateTime: 2023-04-11T00:01:00+00:00 ((2460046j,60s,0n),+0s,2299161j)>

# 両者は等しい
puts d <=> dt_00_00 #=> 0

# dはdt_00_01より小さい
puts d <=> dt_00_01 #=> -1

このことから、以下のcover?は日付の範囲に対して、0時00分で比較していると予想できます。

(Date.today - 365 .. Date.today + 365).cover?(DateTime.now) #=> true

日付の比較におけるRange#include?とRange#cover?の動き

次に、二つのメソッドの内部の動きを見てみます。
簡単のために、日付の範囲は(Date.today - 1 .. Date.today + 1)とします。
こちらも内部の演算がわかりやすいように、以下のようにメソッドをオーバーライドします。

require 'date'

module Foo
  def <=>(other)
    puts "#{self} <=> #{other}"
    super
  end
  def ==(other)
    puts "#{self} == #{other}"
    super
  end
  def ===(other)
    puts "#{self} === #{other}"
    super
  end
  def equal?(other)
    puts "#{self} equal? #{other}"
    super
  end
  def eql?(other)
    puts "#{self} eql? #{other}"
    super
  end
  def hash
    puts "hash #{self}"
    super
  end
  def each(*)
    puts "#{self} each"
    super
  end
end

Date.prepend(Foo)
Range.prepend(Foo)

puts "(Date.today - 1 .. Date.today + 1).include?(DateTime.now)"
puts (Date.today - 1 .. Date.today + 1).include?(DateTime.now)

puts "---"

puts "(Date.today - 1 .. Date.today + 1).cover?(DateTime.now)"
puts (Date.today - 1 .. Date.today + 1).cover?(DateTime.now) 

この実行結果は以下となります。

(Date.today - 1 .. Date.today + 1).include?(DateTime.now)
2023-04-23 <=> 2023-04-25
2023-04-23..2023-04-25 each
2023-04-23 <=> 2023-04-25
2023-04-23 == 2023-04-24T19:10:22+09:00
2023-04-23 <=> 2023-04-24T19:10:22+09:00
2023-04-24 <=> 2023-04-25
2023-04-24 == 2023-04-24T19:10:22+09:00
2023-04-24 <=> 2023-04-24T19:10:22+09:00
2023-04-25 <=> 2023-04-25
2023-04-25 == 2023-04-24T19:10:22+09:00
2023-04-25 <=> 2023-04-24T19:10:22+09:00
false
---
(Date.today - 1 .. Date.today + 1).cover?(DateTime.now)
2023-04-23 <=> 2023-04-25
2023-04-23 <=> 2023-04-24T19:10:22+09:00
2023-04-24T19:10:22+09:00 <=> 2023-04-25
true

まず、cover?の方から見ていくと、こちらは文字列の時と同じく境界と比較していることが分かります。
一方で、include?は、eachにより範囲内の日付それぞれと'2023-04-24T19:10:22+09:00'を比較していることが分かります。

ちなみに、

2023-04-23 <=> 2023-04-25
2023-04-23..2023-04-25 each
2023-04-23 <=> 2023-04-25

でeachの前後で同じものを比較しているのは、範囲オブジェクトを初期化するタイミングで ruby/range.c at v3_2_1 · ruby/ruby の部分で呼ばれています。
この処理は範囲オブジェクトとして初期化可能かを判定しています。

たとえばDate.today + 1 .. '日付でない文字'のような範囲オブジェクトを作ると、Date.today + 1 <=> '日付でない文字' (Cではrb_funcall(beg, id_cmp, 1, end))で比較不能のnilが返ります。

上記のオーバーライドのRange.prepend(Foo)をコメントアウトしても、実行結果に出てくるため、Dateクラスの方に原因がありそうです。一応、Cコードを見てもよくわかりませんでした。
詳しい方がいらっしゃいましたら、教えて頂けると幸いです(そもそもDateクラスのCコードがどこにあるか分かっていません...)。

ruby/string.c at v3_2_1 · ruby/ruby

Range#include? と Range#cover? のベンチマーク結果

最後に、両メソッドの実行速度を比較してみます。
以下のようにベンチマークのコードを用意します。

require 'benchmark'

time = Benchmark.realtime do
  ('ab'..'ad').include?('ac')
end
puts "('ab'..'ad').include?('ac'): #{time}s"


time = Benchmark.realtime do
  ('ab'..'ad').cover?('ac')
end
puts "('ab'..'ad').cover?('ac'): #{time}s"

実行結果は以下のとおりです。

('ab'..'ad').include?('ac'): 8.00006091594696e-06s
('ab'..'ad').cover?('ac'): 1.00000761449337e-06s

上記より、確かにinclude?の方が遅いと思われます。この原因は前節でも述べたように、eachで各要素を比較していることにあると思われます。
ちなみに数値に対して、比較すると以下のようになります。

time = Benchmark.realtime do
  (1..3).include?(2)
end
puts "(1..3).include?(2): #{time}s"


time = Benchmark.realtime do
  (1..3).cover?(2)
end
puts "(1..3).cover?(2): #{time}s"

以下が実行結果です。

(1..3).include?(2): 1.00000761449337e-06s
(1..3).cover?(2): 1.00000761449337e-06s

数値の場合は、include?もC言語上ではcover?と同じ関数を呼びに行っているので、実行時間も同じになります(たまにズレるので参考値です)。

まとめ

結論は最初に挙げた通りです。
改めてざっくりまとめると、

  • include?: 範囲と引数が数値ならば、cover?と同じ動作をする。数値以外なら、メソッドの内部としてはStringとして扱われ、範囲が1文字の文字からなるなら、始点と終点との比較を行う。そうでないなら、各要素に対し、each(Cコード中ではwhile内での実行)を行い、範囲内の各要素と==で一致するか判定する。

  • cover?: 始点と終点に対して、引数を<=>で比較する。include?より処理が早い。

となります。

感想

まだ、Rubyを習い始めてから1ヶ月ですが、裏のC言語を読むと少し理解が深まる気がしました。
やはり、公式ドキュメントとRuby自体のコードをしっかり読むのは大事だと感じたテーマだったと思います。

分からなかったこと

文字列の比較に関して

本題とはずれますが、(b..d).include?(d)の動作に関して、疑問点があります。
「文字列比較におけるRange#include?とRange#cover?の動き」の節において、

ruby/string.c at v3_2_1 · ruby/ruby

if (RSTRING_LEN(beg) == 1 && RSTRING_LEN(end) == 1) {
            if (RSTRING_LEN(val) == 0 || RSTRING_LEN(val) > 1)
                return Qfalse;
            else {
                char b = *bp;
                char e = *ep;
                char v = *vp;

                if (ISASCII(b) && ISASCII(e) && ISASCII(v)) {
                    if (b <= v && v < e) return Qtrue;
                    return RBOOL(!RTEST(exclusive) && v == e);
                }
            }
        }

を引用しました。ここから、(b..d).include?(d)のような1文字同士の比較の場合、引数を境界との文字のバイトで比較しているように見えます。実際、メソッドをオーバーライドすると境界としか比較をしていませんでした。

しかし、ここで述べた処理の下にあるrb_str_upto_each関数内でも以下の処理をしています。

ruby/string.c at v3_2_1 · ruby/ruby

/* single character */
    if (RSTRING_LEN(beg) == 1 && RSTRING_LEN(end) == 1 && ascii) {
        char c = RSTRING_PTR(beg)[0];
        char e = RSTRING_PTR(end)[0];

        if (c > e || (excl && c == e)) return beg;
        for (;;) {
            if ((*each)(rb_enc_str_new(&c, 1, enc), arg)) break;
            if (!excl && c == e) break;
            c++;
            if (excl && c == e) break;
        }
        return beg;
    }

コメントだけ読むと、ここでも1文字の比較をしています。
こちらの処理ではfor文を回しており、各要素と引数を比較してそうですが、eachのコールバック関数がよく分からないため、具体的に何をやっているのか分かっていません。

もし、上記の処理(特にeachのコールバック関数の動作)に関して詳しい方がおりましたら、コメント頂きたいです。
また、なぜinclude?において1文字の比較に対して二つ処理があるのかも、合わせて教えていただきたいです。

日付の比較に関して

前述の通り、以下で<=>が二度出てくる理由が分かりませんでした。もし、分かる方がいらっしゃいましたら、こちらも合わせて教えて頂けると幸いです。

2023-04-23 <=> 2023-04-25
2023-04-23..2023-04-25 each
2023-04-23 <=> 2023-04-25

よろしくお願い致します。

謝辞

ここまでの内容は、自分が所属しているプログラミングスクールであるFjord Boot Campにおいて質問した回答が元となっています。
そのため、以下のお二方の回答を参考にしております。

本当にありがとうございました!!

GitHubで編集を提案

Discussion

Junichi ItoJunichi Ito

@kochi さん、こんにちは。
疑問点に関してわかる範囲でお答えします。

ただしC言語は強くないので、「たぶんこういうことなのでは」という推測で話します。
間違ってたらごめんなさい🙏

上記のオーバーライドのRange.prepend(Foo)をコメントアウトしても、実行結果に出てくるため、Dateクラスの方に原因がありそうです。一応、Cコードを見てもよくわかりませんでした。
詳しい方がいらっしゃいましたら、教えて頂けると幸いです

rb_funcall(beg, id_cmp, 1, end);のコードがDate.today - 1 <=> Date.today + 1に相当する処理なので、Dateクラスの<=>メソッドが呼ばれて"2023-04-23 <=> 2023-04-25"が画面に出力されたんじゃないかなーと推測します。

(そもそもDateクラスのCコードがどこにあるか分かっていません...)。

外部gemになっているのでここですね。

https://github.com/ruby/date

Range#include? と Range#cover? のベンチマーク結果

include?やcover?を1回呼び出すだけでは一瞬で終わってしまうので、1000回とか1万回ぐらい繰り返してある程度の実行時間がかかる状況でパフォーマンスを比較するのが定石です。
詳しくは以下の記事を参考にしてみてください。

https://qiita.com/scivola/items/c5b2aeaf7d67a9ef310a

こちらの処理ではfor文を回しており、各要素と引数を比較してそうですが、eachのコールバック関数がよく分からないため、具体的に何をやっているのか分かっていません。

この処理はrb_str_upto_eachという関数の処理なので、内部的に'b'.upto('d')に相当する処理を実行してるんだと思います。
変数cは手前の流れからアスキー文字のポインタなので、c++で"b→c→d"と一文字ずつ文字をずらしながらループ処理してるものと思われます。

for (;;) {
    if ((*each)(rb_enc_str_new(&c, 1, enc), arg)) break;
    if (!excl && c == e) break;
    c++;
    if (excl && c == e) break;
}

で、uptoメソッドを使う場合、'b'.upto('d') { ??? }のようにブロックを組み合わせますが、この???部分に当たる処理がeachです。そしてこれはこの関数の引数として渡されます。

VALUE
rb_str_upto_each(VALUE beg, VALUE end, int excl, int (*each)(VALUE, VALUE), VALUE arg)

rb_str_upto_eachを呼び出している行はここです。

VALUE
rb_str_include_range_p(VALUE beg, VALUE end, VALUE val, VALUE exclusive)
{
    beg = rb_str_new_frozen(beg);
    StringValue(end);
    end = rb_str_new_frozen(end);
    if (NIL_P(val)) return Qfalse;
    val = rb_check_string_type(val);
    if (NIL_P(val)) return Qfalse;

    /* 略 */

    rb_str_upto_each(beg, end, RTEST(exclusive), include_range_i, (VALUE)&val);

    return RBOOL(NIL_P(val));
}

つまり上で出てきたeachの実態はinclude_range_iという関数です。

static int
include_range_i(VALUE str, VALUE arg)
{
    VALUE *argp = (VALUE *)arg;
    if (!rb_equal(str, *argp)) return 0;
    *argp = Qnil;
    return 1;
}

なので、(*each)(rb_enc_str_new(&c, 1, enc), arg)は、イメージとしてinclude_range_i('b', 'd')include_range_i('c', 'd')include_range_i('d', 'd') のような呼び出しを繰り返すことになります。

include_range_istrargが異なれば偽を返し、同値であれば真を返します。
include_range_iから真が返ってきた場合、if ((*each)(rb_enc_str_new(&c, 1, enc), arg)) break;でbreakして、uptoのループ処理はそこで中断します(そしてreturn beg;で戻り値を返してrb_str_upto_each関数の処理が終わる)。

つまりざっくりまとめると、疑問に思われている部分のコードは、たとえば('b'..'d').include?('d')であれば「"b"と"d"が同値か?」「"c"と"d"が同値か?」「"d"と"d"が同値か?」を繰り返して、真になれば終わる処理、という感じだと思います。

また、なぜinclude?において1文字の比較に対して二つ処理があるのかも、合わせて教えていただきたいです。

上で詳しく説明したrb_str_upto_each関数の処理ですが、('b'..'d').include?('d')のように、範囲オブジェクトやinclude?メソッドの引数が全部アスキー文字1文字だった場合は、おそらくこの処理は呼ばれません。

なぜならrb_str_upto_eachを呼び出す前にこの行でreturnするからです。

VALUE
rb_str_include_range_p(VALUE beg, VALUE end, VALUE val, VALUE exclusive)
{
    /* 略 */

                /* アスキー文字1文字だったらここで関数の処理が終わる */
                if (ISASCII(b) && ISASCII(e) && ISASCII(v)) {
                    if (b <= v && v < e) return Qtrue;
                    return RBOOL(!RTEST(exclusive) && v == e);
                }

    /* 略 */

    /* アスキー文字1文字だったらこの行は呼ばれない(はず) */
    rb_str_upto_each(beg, end, RTEST(exclusive), include_range_i, (VALUE)&val);

    return RBOOL(NIL_P(val));
}

rb_str_upto_each関数はinclude?専用の関数ではなく、uptoメソッドでも呼ばれています(というか、関数名からしてuptoメソッドで呼ばれるのが主目的だと思われる)。

static VALUE
rb_str_upto(int argc, VALUE *argv, VALUE beg)
{
    /* 略 */

    return rb_str_upto_each(beg, end, RTEST(exclusive), str_upto_i, Qnil);
}

なので、'b'.upto('d') { |s| puts s.upcase } みたいに「uptoメソッドのレシーバと引数がどちらもアスキー文字1文字」だった場合に、rb_str_upto_each関数の/* single character */とコメントされているコードが実行されるんだと思います。

よって「なぜinclude?において1文字の比較に対して二つ処理があるのか」という問いに対しては、「アスキー文字1文字の場合、一方はinclude?メソッドで呼ばれ、もう一方はuptoメソッドで呼ばれるのであって、同時に2つが呼び出されるわけではない」というのが回答になります。

kochikochi

コメントありがとうございます!!

自分の疑問に対して、ここまで詳細に答えて頂けるなんて感無量です!
頂いた回答によって、よりRubyのメソッドとC言語に関して詳しくなれました!!
頂いたコメントは、後ほど記事に反映したいと思います。

本当にありがとうございました!!

kochikochi

すみません、以下の点だけよく分からなかったので、再質問させてください。

上記のオーバーライドのRange.prepend(Foo)をコメントアウトしても、実行結果に出てくるため、Dateクラスの方に原因がありそうです。一応、Cコードを見てもよくわかりませんでした。
詳しい方がいらっしゃいましたら、教えて頂けると幸いです

rb_funcall(beg, id_cmp, 1, end);のコードがDate.today - 1 <=> Date.today + 1に相当する処理なので、Dateクラスの<=>メソッドが呼ばれて"2023-04-23 <=> 2023-04-25"が画面に出力されたんじゃないかなーと推測します。

上記に関して、Cコードを追うだけではinclude?の中にrb_funcall(beg, id_cmp, 1, end);が出てこないのですが、どのように繋がっているのでしょうか?
Rangeクラスを見に行っているとは思うのですが、Cコードだけ追うとrange_initでRangeオブジェクトを初期化する場面が出てこないように見えます。

一応、eachのコールバック関数のinclude_range_iを見ると

static int
include_range_i(VALUE str, VALUE arg)
{
    VALUE *argp = (VALUE *)arg;
    if (!rb_equal(str, *argp)) return 0;
    *argp = Qnil;
    return 1;
}

更に、rb_equalを見ると

rb_equal(VALUE obj1, VALUE obj2)
{
    VALUE result;

    if (obj1 == obj2) return Qtrue;
    result = rb_equal_opt(obj1, obj2);
    if (UNDEF_P(result)) {
        result = rb_funcall(obj1, id_eq, 1, obj2);
    }
    return RBOOL(RTEST(result));
}

となり、result = rb_funcall(obj1, id_eq, 1, obj2);にて<=>を行っていることが分かります。自分はこのresult = rb_funcall(obj1, id_eq, 1, obj2);によって<=>が表示されていると思ってましたが、違うのでしょうか?(もし、こちらが呼ばれている場合、Range#eachの前に<=>が出てくるのは説明できません...)

もう一度質問をまとめると、include?のCコード中で、range_init内のrb_funcall(beg, id_cmp, 1, end);をどのように呼びに行っているか教えて頂きたいです。

かなり細かい質問となってしまいますが、もしよろしければご回答頂けると幸いです。
よろしくお願い致します!!

Junichi ItoJunichi Ito

もう一度質問をまとめると、include?のCコード中で、range_init内のrb_funcall(beg, id_cmp, 1, end);をどのように呼びに行っているか教えて頂きたいです。

以下のコードを実行します。

require 'date'

module Foo
  def <=>(other)
    puts "#{self} <=> #{other}"
    super
  end
  def ==(other)
    puts "#{self} == #{other}"
    super
  end
end

Date.prepend(Foo)

puts "Initialize Range"
range = (Date.today - 1 .. Date.today + 1)

puts "call include?"
puts range.include?(DateTime.now)

出力結果はこうなります。

Initialize Range
2023-04-25 <=> 2023-04-27
call include?
2023-04-25 <=> 2023-04-27
2023-04-25 == 2023-04-26T07:33:54+09:00
2023-04-25 <=> 2023-04-26T07:33:54+09:00
2023-04-26 <=> 2023-04-27
2023-04-26 == 2023-04-26T07:33:54+09:00
2023-04-26 <=> 2023-04-26T07:33:54+09:00
2023-04-27 <=> 2023-04-27
2023-04-27 == 2023-04-26T07:33:54+09:00
2023-04-27 <=> 2023-04-26T07:33:54+09:00
false

最初の2行の出力で<=>が呼ばれるのはrange.cの52行目だと思われます。

Initialize Range
2023-04-25 <=> 2023-04-27

つまりrange_initrb_funcall(beg, id_cmp, 1, end);は、include?メソッド経由で呼ばれるのではなく、Rangeクラスのインスタンスを作成したタイミングで呼ばれます。

・・・という説明で疑問は解消するでしょうか?

kochikochi

つまりrange_initのrb_funcall(beg, id_cmp, 1, end);は、include?メソッド経由で呼ばれるのではなく、Rangeクラスのインスタンスを作成したタイミングで呼ばれます。

なるほど、そもそも( .. )の範囲オブジェクトを作成時にRangeクラスのインスタンスが作成され、同時にrb_range_newが呼び出され、更にその関数内のrange_init内のrb_funcall(beg, id_cmp, 1, end);が実行されるということですね。
結果として( .. )作成時に<=>が実行される形ですね。

勘違いしていて、ずっとinclude?のメソッド内を追ってました..笑

分かりやすいご説明本当にありがとうございます!
先の回答を含め、本当に勉強になりました!!
記事には随時反映していきます😆
ありがとうございました!!!