[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:
parent
ee7a338d2b
commit
8c272f4481
Notes:
git
2022-12-16 13:53:21 +00:00
@ -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)
|
||||
|
@ -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
111
time.c
@ -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)
|
||||
{
|
||||
|
15
timev.rb
15
timev.rb
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user