RubyKaigiでRubyの改善のために、次の協力が求められていました。
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._strptime
やDate.strptime
、Date#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
/* * 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
を呼び出しています。
/* * 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の実装のようです。 引数のフォーマット文字列がわずかに変わっています。
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
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)
を呼び出しています。
ここで文字列をパースしているようです。
Date._strptime
/* * 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
/* * 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)
を呼び出しています。
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
/* * 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クラスを作っているように見えます。 この辺なら手が出せそうに思えます。