[Feature #18033] Make Time.new parse time strings

`Time.new` now parses strings such as the result of `Time#inspect`
and restricted ISO-8601 formats.
This commit is contained in:
Nobuyoshi Nakada 2021-09-05 00:24:23 +09:00
parent ee7a338d2b
commit 8c272f4481
Notes: git 2022-12-16 13:53:21 +00:00
4 changed files with 146 additions and 3 deletions

View File

@ -6,3 +6,5 @@ benchmark:
- Time.iso8601(iso8601)
- Time.parse(iso8601)
- Time.parse(inspect)
- Time.new(iso8601) rescue Time.iso8601(iso8601)
- Time.new(inspect) rescue Time.parse(inspect)

View File

@ -57,6 +57,27 @@ class TestTime < Test::Unit::TestCase
assert_equal([0, 0, 0, 2, 1, 2000], Time.new(2000, 1, 1, 24, 0, 0, "-00:00").to_a[0, 6])
end
def test_new_from_string
assert_raise(ArgumentError) { Time.new(2021, 1, 1, "+09:99") }
t = Time.utc(2020, 12, 24, 15, 56, 17)
assert_equal(t, Time.new("2020-12-24T15:56:17Z"))
assert_equal(t, Time.new("2020-12-25 00:56:17 +09:00"))
assert_equal(t, Time.new("2020-12-25 00:57:47 +09:01:30"))
assert_equal(t, Time.new("2020-12-25 00:56:17 +0900"))
assert_equal(t, Time.new("2020-12-25 00:57:47 +090130"))
assert_equal(t, Time.new("2020-12-25T00:56:17+09:00"))
assert_equal(Time.utc(2020, 12, 24, 15, 56, 0), Time.new("2020-12-25 00:56 +09:00"))
assert_equal(Time.utc(2020, 12, 24, 15, 0, 0), Time.new("2020-12-25 00 +09:00"))
assert_equal(Time.new(2021, 12, 25, in: "+09:00"), Time.new("2021-12-25+09:00"))
assert_equal(0.123456r, Time.new("2021-12-25 00:00:00.123456 +09:00").subsec)
assert_raise_with_message(ArgumentError, "subsecond expected after dot: 00:56:17. ") {
Time.new("2020-12-25 00:56:17. +0900")
}
end
def test_time_add()
assert_equal(Time.utc(2000, 3, 21, 3, 30) + 3 * 3600,
Time.utc(2000, 3, 21, 6, 30))

111
time.c
View File

@ -2350,7 +2350,8 @@ vtm_day_wraparound(struct vtm *vtm)
}
static VALUE
time_init_args(rb_execution_context_t *ec, VALUE time, VALUE year, VALUE mon, VALUE mday, VALUE hour, VALUE min, VALUE sec, VALUE zone)
time_init_args(rb_execution_context_t *ec, VALUE time, VALUE year, VALUE mon, VALUE mday,
VALUE hour, VALUE min, VALUE sec, VALUE subsec, VALUE zone)
{
struct vtm vtm;
VALUE utc = Qnil;
@ -2374,6 +2375,10 @@ time_init_args(rb_execution_context_t *ec, VALUE time, VALUE year, VALUE mon, VA
vtm.sec = 0;
vtm.subsecx = INT2FIX(0);
}
else if (!NIL_P(subsec)) {
vtm.sec = obj2ubits(sec, 6);
vtm.subsecx = subsec;
}
else {
VALUE subsecx;
vtm.sec = obj2subsecx(sec, &subsecx);
@ -2443,6 +2448,110 @@ time_init_args(rb_execution_context_t *ec, VALUE time, VALUE year, VALUE mon, VA
}
}
static int
two_digits(const char *ptr, const char *end, const char **endp)
{
ssize_t len = end - ptr;
if (len < 2) return -1;
if (!ISDIGIT(ptr[0]) || !ISDIGIT(ptr[1])) return -1;
if ((len > 2) && ISDIGIT(ptr[2])) return -1;
*endp = ptr + 2;
return (ptr[0] - '0') * 10 + (ptr[1] - '0');
}
static VALUE
parse_int(const char *ptr, const char *end, const char **endp, size_t *ndigits, bool sign)
{
ssize_t len = (end - ptr);
int flags = sign ? RB_INT_PARSE_SIGN : 0;
return rb_int_parse_cstr(ptr, len, (char **)endp, ndigits, 10, flags);
}
static VALUE
time_init_parse(rb_execution_context_t *ec, VALUE time, VALUE str, VALUE zone)
{
if (NIL_P(str = rb_check_string_type(str))) return Qnil;
if (!rb_enc_str_asciicompat_p(str)) {
rb_raise(rb_eArgError, "time string should have ASCII compatible encoding");
}
const char *const begin = RSTRING_PTR(str);
const char *const end = RSTRING_END(str);
const char *ptr = begin;
VALUE year = Qnil, subsec = Qnil;
int mon = -1, mday = -1, hour = -1, min = -1, sec = -1;
size_t ndigits;
while ((ptr < end) && ISSPACE(*ptr)) ptr++;
year = parse_int(ptr, end, &ptr, &ndigits, true);
if (NIL_P(year)) {
rb_raise(rb_eArgError, "can't parse: %+"PRIsVALUE, str);
}
do {
#define peek_n(c, n) ((ptr + n < end) && ((unsigned char)ptr[n] == (c)))
#define peek(c) peek_n(c, 0)
#define peekc_n(n) ((ptr + n < end) ? (int)(unsigned char)ptr[n] : -1)
#define peekc() peekc_n(0)
if (!peek('-')) break;
if ((mon = two_digits(ptr + 1, end, &ptr)) < 0) break;
if (!peek('-')) break;
if ((mday = two_digits(ptr + 1, end, &ptr)) < 0) break;
if (!peek(' ') && !peek('T')) break;
const char *const time_part = ptr + 1;
if ((hour = two_digits(ptr + 1, end, &ptr)) < 0) break;
if (!peek(':')) break;
if ((min = two_digits(ptr + 1, end, &ptr)) < 0) break;
if (!peek(':')) break;
if ((sec = two_digits(ptr + 1, end, &ptr)) < 0) break;
if (peek('.')) {
ptr++;
for (ndigits = 0; ndigits < 45 && ISDIGIT(peekc_n(ndigits)); ++ndigits);
if (!ndigits) {
int clen = rb_enc_precise_mbclen(ptr, end, rb_enc_get(str));
if (clen < 0) clen = 0;
rb_raise(rb_eArgError, "subsecond expected after dot: %.*s",
(int)(ptr - time_part) + clen, time_part);
}
subsec = parse_int(ptr, ptr + ndigits, &ptr, &ndigits, false);
if (NIL_P(subsec)) break;
while (ptr < end && ISDIGIT(*ptr)) ptr++;
}
} while (0);
while (ptr < end && ISSPACE(*ptr)) ptr++;
const char *const zstr = ptr;
while (ptr < end && !ISSPACE(*ptr)) ptr++;
const char *const zend = ptr;
while (ptr < end && ISSPACE(*ptr)) ptr++;
if (ptr < end) {
VALUE mesg = rb_str_new_cstr("can't parse at: ");
rb_str_cat(mesg, ptr, end - ptr);
rb_exc_raise(rb_exc_new_str(rb_eArgError, mesg));
}
if (zend > zstr) {
zone = rb_str_subseq(str, zstr - begin, zend - zstr);
}
if (!NIL_P(subsec)) {
/* subseconds is the last using ndigits */
if (ndigits < 9) {
VALUE mul = rb_int_positive_pow(10, 9 - ndigits);
subsec = rb_int_mul(subsec, mul);
}
else if (ndigits > 9) {
VALUE num = rb_int_positive_pow(10, ndigits - 9);
subsec = rb_rational_new(subsec, num);
}
}
#define non_negative(x) ((x) < 0 ? Qnil : INT2FIX(x))
return time_init_args(ec, time, year,
non_negative(mon),
non_negative(mday),
non_negative(hour),
non_negative(min),
non_negative(sec),
subsec, zone);
}
static void
subsec_normalize(time_t *secp, long *subsecp, const long maxsubsec)
{

View File

@ -294,6 +294,13 @@ class Time
#
# Time.new # => 2021-04-24 17:27:46.0512465 -0500
#
# With one string argument that represents a time, returns a new
# \Time object based on the given argument, in the local timezone.
#
# Time.new('2000-12-31 23:59:59.5') # => 2000-12-31 23:59:59.5 -0600
# Time.new('2000-12-31 23:59:59.5 +0900') # => 2000-12-31 23:59:59.5 +0900
# Time.new('2000-12-31 23:59:59.5', in: '+0900') # => 2000-12-31 23:59:59.5 +0900
#
# With one to six arguments, returns a new \Time object
# based on the given arguments, in the local timezone.
#
@ -368,7 +375,7 @@ class Time
# Time.new(in: '-12:00')
# # => 2022-08-23 08:49:26.1941467 -1200
#
def initialize(year = (now = true), mon = nil, mday = nil, hour = nil, min = nil, sec = nil, zone = nil, in: nil)
def initialize(year = (now = true), mon = (str = year; nil), mday = nil, hour = nil, min = nil, sec = nil, zone = nil, in: nil)
if zone
if Primitive.arg!(:in)
raise ArgumentError, "timezone argument given as positional and keyword arguments"
@ -381,6 +388,10 @@ class Time
return Primitive.time_init_now(zone)
end
Primitive.time_init_args(year, mon, mday, hour, min, sec, zone)
if str and Primitive.time_init_parse(str, zone)
return self
end
Primitive.time_init_args(year, mon, mday, hour, min, sec, nil, zone)
end
end