@ledsun blog

無味の味は佳境に入らざればすなわち知れず

RubyのDateからTimeへ移行する道程、いまここ

RubyKaigiでRubyの改善のために、次の協力が求められていました。

youtu.be

RubyのDateライブラリーのメンテナンスが行われていません。 Dateと同じ機能がほとんどTimeで提供されています。 strptime, parseなどはDateの機能が残っています。 この機能をTimeに置き換える作業をする人が求められています。 将来的にはDateを標準添付Gemから外したいそうです。

具体的にはどのような作業が必要なのでしょうか?

Timeの実装

Time.strptimeの実装をみてみます。

https://github.com/ruby/ruby/blob/a65ac2d6fa4e7381d88b79a2881f7c05daa903c3/lib/time.rb#L451-L481

    # You must require 'time' to use this method.
    #
    def strptime(date, format, now=self.now)
      d = Date._strptime(date, format)
      raise ArgumentError, "invalid date or strptime format - `#{date}' `#{format}'" unless d
      if seconds = d[:seconds]
        if sec_fraction = d[:sec_fraction]
          usec = sec_fraction * 1000000
          usec *= -1 if seconds < 0
        else
          usec = 0
        end
        t = Time.at(seconds, usec)
        if zone = d[:zone]
          force_zone!(t, zone)
        end
      else
        year = d[:year]
        year = yield(year) if year && block_given?
        yday = d[:yday]
        if (d[:cwyear] && !year) || ((d[:cwday] || d[:cweek]) && !(d[:mon] && d[:mday]))
          # make_time doesn't deal with cwyear/cwday/cweek
          return Date.strptime(date, format).to_time
        end
        if (d[:wnum0] || d[:wnum1]) && !yday && !(d[:mon] && d[:mday])
          yday = Date.strptime(date, format).yday
        end
        t = make_time(date, year, yday, d[:mon], d[:mday], d[:hour], d[:min], d[:sec], d[:sec_fraction], d[:zone], now)
      end
      t
    end

Date._strptimeDate.strptimeDate#to_timeと言ったDateのメソッドが使われています。 これではDateを標準添付Gemから外せません。

Time.parseも同様です。

https://github.com/ruby/ruby/blob/a65ac2d6fa4e7381d88b79a2881f7c05daa903c3/lib/time.rb#L376-L384

    # You must require 'time' to use this method.
    #
    def parse(date, now=self.now)
      comp = !block_given?
      d = Date._parse(date, comp)
      year = d[:year]
      year = yield(year) if year && !comp
      make_time(date, year, d[:yday], d[:mon], d[:mday], d[:hour], d[:min], d[:sec], d[:sec_fraction], d[:zone], now)
    end

Dateの仕様

使われているDateのメソッドの仕様を確認しておきましょう。

Date._strptime

Date._strptime (Ruby 3.1.0 リファレンスマニュアル)

このメソッドは Date.strptime と似ていますが、日付オブジェクトを生成せずに、見いだした要素をハッシュで返します。

Date.strptime

Date.strptime (Ruby 3.1.0 リファレンスマニュアル)

与えられた雛型で日付表現を解析し、その情報に基づいて日付オブジェクトを生成します。

Date#to_time

https://docs.ruby-lang.org/ja/master/class/Date.html#I_TO_TIME

対応する Time オブジェクトを返します。

Date._parse

https://docs.ruby-lang.org/ja/master/class/Date.html#S__PARSE

このメソッドは Date.parse と似ていますが、日付オブジェクトを生成せずに、見いだした要素をハッシュで返します。

Dateの実装

置き換え元のDateの実装も見てみましょう。

Date._strptime

https://github.com/ruby/ruby/blob/745287d43a8fb63c84be986b23319d40e6affe2f/ext/date/date_core.c#L4266-L4283

/*
 * call-seq:
 *    Date._strptime(string[, format='%F'])  ->  hash
 *
 * Parses the given representation of date and time with the given
 * template, and returns a hash of parsed elements.  _strptime does
 * not support specification of flags and width unlike strftime.
 *
 *    Date._strptime('2001-02-03', '%Y-%m-%d')
 *             #=> {:year=>2001, :mon=>2, :mday=>3}
 *
 * See also strptime(3) and #strftime.
 */
static VALUE
date_s__strptime(int argc, VALUE *argv, VALUE klass)
{
    return date_s__strptime_internal(argc, argv, klass, "%F");
}

date_s__strptime_internalを呼び出しています。

https://github.com/ruby/ruby/blob/745287d43a8fb63c84be986b23319d40e6affe2f/ext/date/date_core.c#L7945-L7959

/*
 * call-seq:
 *    DateTime._strptime(string[, format='%FT%T%z'])  ->  hash
 *
 * Parses the given representation of date and time with the given
 * template, and returns a hash of parsed elements.  _strptime does
 * not support specification of flags and width unlike strftime.
 *
 * See also strptime(3) and #strftime.
 */
static VALUE
datetime_s__strptime(int argc, VALUE *argv, VALUE klass)
{
    return date_s__strptime_internal(argc, argv, klass, "%FT%T%z");
}

ほぼそっくりのコードがありますが、これはDateTimeの実装のようです。 引数のフォーマット文字列がわずかに変わっています。

https://github.com/ruby/ruby/blob/745287d43a8fb63c84be986b23319d40e6affe2f/ext/date/date_core.c#L4217-L4264

static VALUE
date_s__strptime_internal(int argc, VALUE *argv, VALUE klass,
              const char *default_fmt)
{
    VALUE vstr, vfmt, hash;
    const char *str, *fmt;
    size_t slen, flen;

    rb_scan_args(argc, argv, "11", &vstr, &vfmt);

    StringValue(vstr);
    if (!rb_enc_str_asciicompat_p(vstr))
    rb_raise(rb_eArgError,
         "string should have ASCII compatible encoding");
    str = RSTRING_PTR(vstr);
    slen = RSTRING_LEN(vstr);
    if (argc < 2) {
    fmt = default_fmt;
    flen = strlen(default_fmt);
    }
    else {
    StringValue(vfmt);
    if (!rb_enc_str_asciicompat_p(vfmt))
        rb_raise(rb_eArgError,
             "format should have ASCII compatible encoding");
    fmt = RSTRING_PTR(vfmt);
    flen = RSTRING_LEN(vfmt);
    }
    hash = rb_hash_new();
    if (NIL_P(date__strptime(str, slen, fmt, flen, hash)))
    return Qnil;

    {
    VALUE zone = ref_hash("zone");
    VALUE left = ref_hash("leftover");

    if (!NIL_P(zone)) {
        rb_enc_copy(zone, vstr);
        set_hash("zone", zone);
    }
    if (!NIL_P(left)) {
        rb_enc_copy(left, vstr);
        set_hash("leftover", left);
    }
    }

    return hash;
}

date__strptime(str, slen, fmt, flen, hash)を呼び出しています。

date__strptime

https://github.com/ruby/ruby/blob/745287d43a8fb63c84be986b23319d40e6affe2f/ext/date/date_strptime.c#L654-L697

VALUE
date__strptime(const char *str, size_t slen,
           const char *fmt, size_t flen, VALUE hash)
{
    size_t si;
    VALUE cent, merid;

    si = date__strptime_internal(str, slen, fmt, flen, hash);

    if (slen > si) {
    VALUE s;

    s = rb_usascii_str_new(&str[si], slen - si);
    set_hash("leftover", s);
    }

    if (fail_p())
    return Qnil;

    cent = del_hash("_cent");
    if (!NIL_P(cent)) {
    VALUE year;

    year = ref_hash("cwyear");
    if (!NIL_P(year))
        set_hash("cwyear", f_add(year, f_mul(cent, INT2FIX(100))));
    year = ref_hash("year");
    if (!NIL_P(year))
        set_hash("year", f_add(year, f_mul(cent, INT2FIX(100))));
    }

    merid = del_hash("_merid");
    if (!NIL_P(merid)) {
    VALUE hour;

    hour = ref_hash("hour");
    if (!NIL_P(hour)) {
        hour = f_mod(hour, INT2FIX(12));
        set_hash("hour", f_add(hour, merid));
    }
    }

    return hash;
}

date__strptime_internal(str, slen, fmt, flen, hash)を呼び出しています。

https://github.com/ruby/ruby/blob/745287d43a8fb63c84be986b23319d40e6affe2f/ext/date/date_strptime.c#L164-L652

ここで文字列をパースしているようです。

Date._strptime

https://github.com/ruby/ruby/blob/745287d43a8fb63c84be986b23319d40e6affe2f/ext/date/date_core.c#L4285-L4327

/*
 * call-seq:
 *    Date.strptime([string='-4712-01-01'[, format='%F'[, start=Date::ITALY]]])  ->  date
 *
 * Parses the given representation of date and time with the given
 * template, and creates a date object.  strptime does not support
 * specification of flags and width unlike strftime.
 *
 *    Date.strptime('2001-02-03', '%Y-%m-%d')  #=> #<Date: 2001-02-03 ...>
 *    Date.strptime('03-02-2001', '%d-%m-%Y')  #=> #<Date: 2001-02-03 ...>
 *    Date.strptime('2001-034', '%Y-%j')   #=> #<Date: 2001-02-03 ...>
 *    Date.strptime('2001-W05-6', '%G-W%V-%u') #=> #<Date: 2001-02-03 ...>
 *    Date.strptime('2001 04 6', '%Y %U %w')   #=> #<Date: 2001-02-03 ...>
 *    Date.strptime('2001 05 6', '%Y %W %u')   #=> #<Date: 2001-02-03 ...>
 *    Date.strptime('sat3feb01', '%a%d%b%y')   #=> #<Date: 2001-02-03 ...>
 *
 * See also strptime(3) and #strftime.
 */
static VALUE
date_s_strptime(int argc, VALUE *argv, VALUE klass)
{
    VALUE str, fmt, sg;

    rb_scan_args(argc, argv, "03", &str, &fmt, &sg);

    switch (argc) {
      case 0:
    str = rb_str_new2("-4712-01-01");
      case 1:
    fmt = rb_str_new2("%F");
      case 2:
    sg = INT2FIX(DEFAULT_SG);
    }

    {
    VALUE argv2[2], hash;

    argv2[0] = str;
    argv2[1] = fmt;
    hash = date_s__strptime(2, argv2, klass);
    return d_new_by_frags(klass, hash, sg);
    }
}

Date._strptimeの実装関数date_s__strptimeを呼び出して、Dateクラスに変換しているようです。

Date._parse

https://github.com/ruby/ruby/blob/745287d43a8fb63c84be986b23319d40e6affe2f/ext/date/date_core.c#L4349-L4371

/*
 * call-seq:
 *    Date._parse(string[, comp=true])  ->  hash
 *
 * Parses the given representation of date and time, and returns a
 * hash of parsed elements.
 *
 * This method *does not* function as a validator.  If the input
 * string does not match valid formats strictly, you may get a cryptic
 * result.  Should consider to use `Date._strptime` or
 * `DateTime._strptime` instead of this method as possible.
 *
 * If the optional second argument is true and the detected year is in
 * the range "00" to "99", considers the year a 2-digit form and makes
 * it full.
 *
 *    Date._parse('2001-02-03')    #=> {:year=>2001, :mon=>2, :mday=>3}
 */
static VALUE
date_s__parse(int argc, VALUE *argv, VALUE klass)
{
    return date_s__parse_internal(argc, argv, klass);
}

date_s__parse_internal(argc, argv, klass)を呼び出しています。

https://github.com/ruby/ruby/blob/745287d43a8fb63c84be986b23319d40e6affe2f/ext/date/date_core.c#L4331-L4347

static VALUE
date_s__parse_internal(int argc, VALUE *argv, VALUE klass)
{
    VALUE vstr, vcomp, hash;

    rb_scan_args(argc, argv, "11", &vstr, &vcomp);
    StringValue(vstr);
    if (!rb_enc_str_asciicompat_p(vstr))
    rb_raise(rb_eArgError,
         "string should have ASCII compatible encoding");
    if (argc < 2)
    vcomp = Qtrue;

    hash = date__parse(vstr, vcomp);

    return hash;
}

date__parse(vstr, vcomp)を呼び出しています。

date__parse

https://github.com/ruby/ruby/blob/master/ext/date/date_parse.c#L2089-L2250

VALUE
date__parse(VALUE str, VALUE comp)
{
    VALUE backref, hash;

#ifdef TIGHT_PARSER
    if (have_invalid_char_p(str))
    PARSER_ERROR;
#endif

    backref = rb_backref_get();
    rb_match_busy(backref);

    {
    static const char pat_source[] =
#ifndef TIGHT_PARSER
        "[^-+',./:@[:alnum:]\\[\\]]+"
#else
        "[^[:graph:]]+"
#endif
        ;
    static VALUE pat = Qnil;

    REGCOMP_0(pat);
    str = rb_str_dup(str);
    f_gsub_bang(str, pat, asp_string());
    }

    hash = rb_hash_new();
    set_hash("_comp", comp);

    if (HAVE_ELEM_P(HAVE_ALPHA))
    parse_day(str, hash);
    if (HAVE_ELEM_P(HAVE_DIGIT))
    parse_time(str, hash);

#ifdef TIGHT_PARSER
    if (HAVE_ELEM_P(HAVE_ALPHA))
    parse_era(str, hash);
#endif

    if (HAVE_ELEM_P(HAVE_ALPHA|HAVE_DIGIT)) {
    if (parse_eu(str, hash))
        goto ok;
    if (parse_us(str, hash))
        goto ok;
    }
    if (HAVE_ELEM_P(HAVE_DIGIT|HAVE_DASH))
    if (parse_iso(str, hash))
        goto ok;
    if (HAVE_ELEM_P(HAVE_DIGIT|HAVE_DOT))
    if (parse_jis(str, hash))
        goto ok;
    if (HAVE_ELEM_P(HAVE_ALPHA|HAVE_DIGIT|HAVE_DASH))
    if (parse_vms(str, hash))
        goto ok;
    if (HAVE_ELEM_P(HAVE_DIGIT|HAVE_SLASH))
    if (parse_sla(str, hash))
        goto ok;
#ifdef TIGHT_PARSER
    if (HAVE_ELEM_P(HAVE_ALPHA|HAVE_DIGIT|HAVE_SLASH)) {
    if (parse_sla2(str, hash))
        goto ok;
    if (parse_sla3(str, hash))
        goto ok;
    }
#endif
    if (HAVE_ELEM_P(HAVE_DIGIT|HAVE_DOT))
    if (parse_dot(str, hash))
        goto ok;
#ifdef TIGHT_PARSER
    if (HAVE_ELEM_P(HAVE_ALPHA|HAVE_DIGIT|HAVE_DOT)) {
    if (parse_dot2(str, hash))
        goto ok;
    if (parse_dot3(str, hash))
        goto ok;
    }
#endif
    if (HAVE_ELEM_P(HAVE_DIGIT))
    if (parse_iso2(str, hash))
        goto ok;
    if (HAVE_ELEM_P(HAVE_DIGIT))
    if (parse_year(str, hash))
        goto ok;
    if (HAVE_ELEM_P(HAVE_ALPHA))
    if (parse_mon(str, hash))
        goto ok;
    if (HAVE_ELEM_P(HAVE_DIGIT))
    if (parse_mday(str, hash))
        goto ok;
    if (HAVE_ELEM_P(HAVE_DIGIT))
    if (parse_ddd(str, hash))
        goto ok;

#ifdef TIGHT_PARSER
    if (parse_wday_only(str, hash))
    goto ok;
    if (parse_time_only(str, hash))
        goto ok;
    if (parse_wday_and_time(str, hash))
    goto ok;

    PARSER_ERROR; /* not found */
#endif

  ok:
#ifndef TIGHT_PARSER
    if (HAVE_ELEM_P(HAVE_ALPHA))
    parse_bc(str, hash);
    if (HAVE_ELEM_P(HAVE_DIGIT))
    parse_frag(str, hash);
#endif

    {
        if (RTEST(del_hash("_bc"))) {
        VALUE y;

        y = ref_hash("cwyear");
        if (!NIL_P(y)) {
        y = f_add(f_negate(y), INT2FIX(1));
        set_hash("cwyear", y);
        }
        y = ref_hash("year");
        if (!NIL_P(y)) {
        y = f_add(f_negate(y), INT2FIX(1));
        set_hash("year", y);
        }
    }

        if (RTEST(del_hash("_comp"))) {
        VALUE y;

        y = ref_hash("cwyear");
        if (!NIL_P(y))
        if (f_ge_p(y, INT2FIX(0)) && f_le_p(y, INT2FIX(99))) {
            if (f_ge_p(y, INT2FIX(69)))
            set_hash("cwyear", f_add(y, INT2FIX(1900)));
            else
            set_hash("cwyear", f_add(y, INT2FIX(2000)));
        }
        y = ref_hash("year");
        if (!NIL_P(y))
        if (f_ge_p(y, INT2FIX(0)) && f_le_p(y, INT2FIX(99))) {
            if (f_ge_p(y, INT2FIX(69)))
            set_hash("year", f_add(y, INT2FIX(1900)));
            else
            set_hash("year", f_add(y, INT2FIX(2000)));
        }
    }

    }

    {
    VALUE zone = ref_hash("zone");
    if (!NIL_P(zone) && NIL_P(ref_hash("offset")))
        set_hash("offset", date_zone_to_diff(zone));
    }

    rb_backref_set(backref);

    return hash;
}

ifdefはヤバいですね。どう扱えばいいのか、よくわかりません。

Date#to_time

https://github.com/ruby/ruby/blob/745287d43a8fb63c84be986b23319d40e6affe2f/ext/date/date_core.c#L4331-L4347

/*
 * call-seq:
 *    d.to_time  ->  time
 *
 * Returns a Time object which denotes self. If self is a julian date,
 * convert it to a gregorian date before converting it to Time.
 */
static VALUE
date_to_time(VALUE self)
{
    get_d1a(self);

    if (m_julian_p(adat)) {
        VALUE tmp = d_lite_gregorian(self);
        get_d1b(tmp);
        adat = bdat;
    }

    return f_local3(rb_cTime,
        m_real_year(adat),
        INT2FIX(m_mon(adat)),
        INT2FIX(m_mday(adat)));
}

年月日を取ってTimeクラスを作っているように見えます。 この辺なら手が出せそうに思えます。

参考