Skip to content

Commit 1112b40

Browse files
andrykonchineregon
authored andcommitted
[GR-18163] Fix Time.new with String argument and handle nanoseconds correctly
PullRequest: truffleruby/4521
2 parents 84a2a3a + afb9130 commit 1112b40

File tree

6 files changed

+68
-24
lines changed

6 files changed

+68
-24
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ New features:
66
Bug fixes:
77

88
* Fix `Range#cover?` on begin-less ranges and non-integer values (@nirvdrum, @rwstauner).
9+
* Fix `Time.new` with String argument and handle nanoseconds correctly (#3836, @andrykonchin).
910

1011
Compatibility:
1112

spec/ruby/core/time/new_spec.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,36 @@ def obj.to_int; 3; end
524524
Time.new("2021-12-25 00:00:00.123456789876 +09:00", precision: 3r).subsec.should == 0.123r
525525
end
526526

527+
it "returns Time with correct subseconds when given seconds fraction is shorted than 6 digits" do
528+
Time.new("2020-12-25T00:56:17.123 +09:00").nsec.should == 123000000
529+
Time.new("2020-12-25T00:56:17.123 +09:00").usec.should == 123000
530+
Time.new("2020-12-25T00:56:17.123 +09:00").subsec.should == 0.123
531+
end
532+
533+
it "returns Time with correct subseconds when given seconds fraction is milliseconds" do
534+
Time.new("2020-12-25T00:56:17.123456 +09:00").nsec.should == 123456000
535+
Time.new("2020-12-25T00:56:17.123456 +09:00").usec.should == 123456
536+
Time.new("2020-12-25T00:56:17.123456 +09:00").subsec.should == 0.123456
537+
end
538+
539+
it "returns Time with correct subseconds when given seconds fraction is longer that 6 digits but shorted than 9 digits" do
540+
Time.new("2020-12-25T00:56:17.12345678 +09:00").nsec.should == 123456780
541+
Time.new("2020-12-25T00:56:17.12345678 +09:00").usec.should == 123456
542+
Time.new("2020-12-25T00:56:17.12345678 +09:00").subsec.should == 0.12345678
543+
end
544+
545+
it "returns Time with correct subseconds when given seconds fraction is nanoseconds" do
546+
Time.new("2020-12-25T00:56:17.123456789 +09:00").nsec.should == 123456789
547+
Time.new("2020-12-25T00:56:17.123456789 +09:00").usec.should == 123456
548+
Time.new("2020-12-25T00:56:17.123456789 +09:00").subsec.should == 0.123456789
549+
end
550+
551+
it "returns Time with correct subseconds when given seconds fraction is longer than 9 digits" do
552+
Time.new("2020-12-25T00:56:17.123456789876 +09:00").nsec.should == 123456789
553+
Time.new("2020-12-25T00:56:17.123456789876 +09:00").usec.should == 123456
554+
Time.new("2020-12-25T00:56:17.123456789876 +09:00").subsec.should == 0.123456789
555+
end
556+
527557
ruby_version_is ""..."3.3" do
528558
it "raise TypeError is can't convert precision keyword argument into Integer" do
529559
-> {

spec/ruby/core/time/shared/time_params.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@
179179
}.should raise_error(ArgumentError, "argument out of range")
180180
end
181181

182+
it "raises ArgumentError when given 8 arguments" do
183+
-> { Time.send(@method, *[0]*8) }.should raise_error(ArgumentError)
184+
end
185+
182186
it "raises ArgumentError when given 9 arguments" do
183187
-> { Time.send(@method, *[0]*9) }.should raise_error(ArgumentError)
184188
end

src/main/java/org/truffleruby/core/time/TimeNodes.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,7 @@ RubyTime timeSFromArray(
532532
Object utcoffset) {
533533
final RubyLanguage language = getLanguage();
534534

535-
if (nsec < 0 || nsec > 999999999 ||
535+
if (nsec < 0 || nsec > 999_999_999 ||
536536
sec < 0 || sec > 60 || // MRI accepts sec=60, whether it is a leap second or not
537537
min < 0 || min > 59 ||
538538
hour < 0 || hour > 23 ||

src/main/ruby/truffleruby/core/time.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -421,11 +421,11 @@ def new(year = undefined, month = undefined, day = nil, hour = nil, minute = nil
421421
elsif Primitive.is_a?(year, String) && month_undefined
422422
Truffle::TimeOperations.new_from_string(self, year, **options)
423423
elsif Primitive.nil? utc_offset
424-
Truffle::TimeOperations.compose(self, :local, year, month, day, hour, minute, second)
424+
Truffle::TimeOperations.compose(self, :local, year, month, day, hour, minute, second, nil, nil)
425425
elsif utc_offset == :std
426-
Truffle::TimeOperations.compose(self, :local, second, minute, hour, day, month, year, nil, nil, false, nil)
426+
Truffle::TimeOperations.compose(self, :local, year, month, day, hour, minute, second, nil, false)
427427
elsif utc_offset == :dst
428-
Truffle::TimeOperations.compose(self, :local, second, minute, hour, day, month, year, nil, nil, true, nil)
428+
Truffle::TimeOperations.compose(self, :local, year, month, day, hour, minute, second, nil, true)
429429
else
430430
if utc_offset_in_utc?(utc_offset)
431431
utc_offset = :utc
@@ -435,7 +435,7 @@ def new(year = undefined, month = undefined, day = nil, hour = nil, minute = nil
435435
utc_offset = Truffle::TimeOperations.calculate_utc_offset_with_timezone_object(utc_offset, :local_to_utc, as_utc) ||
436436
Truffle::TimeOperations.coerce_to_utc_offset(utc_offset)
437437
end
438-
result = Truffle::TimeOperations.compose(self, utc_offset, year, month, day, hour, minute, second)
438+
result = Truffle::TimeOperations.compose(self, utc_offset, year, month, day, hour, minute, second, nil, nil)
439439
Truffle::TimeOperations.set_zone_if_object(result, zone)
440440
result
441441
end
@@ -464,12 +464,12 @@ def now(**options)
464464
end
465465

466466
def local(*args)
467-
Truffle::TimeOperations.compose(self, :local, *args)
467+
Truffle::TimeOperations.compose_dual_signature(self, :local, *args)
468468
end
469469
alias_method :mktime, :local
470470

471471
def gm(*args)
472-
Truffle::TimeOperations.compose(self, :utc, *args)
472+
Truffle::TimeOperations.compose_dual_signature(self, :utc, *args)
473473
end
474474
alias_method :utc, :gm
475475
end

src/main/ruby/truffleruby/core/truffle/time_operations.rb

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,30 @@ module TimeOperations
1717

1818
# Handles Time methods .utc, .gm. .local, .mktime dual signature:
1919
# - year, month, day, hour, min, sec, usec
20-
# - sec, min, hour, day, month, year
21-
def self.compose(time_class, utc_offset, p1, p2 = nil, p3 = nil, p4 = nil, p5 = nil, p6 = nil, p7 = nil,
22-
yday = undefined, is_dst = undefined, tz = undefined)
20+
# - sec, min, hour, day, month, year, dummy, dummy, dummy, dummy
21+
def self.compose_dual_signature(time_class, utc_offset, p1, p2 = nil, p3 = nil, p4 = nil, p5 = nil, p6 = nil, p7 = nil,
22+
yday = undefined, is_dst = undefined, tz = undefined)
2323
if Primitive.undefined?(tz)
2424
unless Primitive.undefined?(is_dst)
2525
raise ArgumentError, 'wrong number of arguments (9 for 1..8)'
2626
end
2727

28-
year, month, mday, hour, min, sec, usec, is_dst = p1, p2, p3, p4, p5, p6, p7, -1
28+
year, month, mday, hour, min, sec, usec, is_dst = p1, p2, p3, p4, p5, p6, p7, nil
2929
else
30-
year, month, mday, hour, min, sec, usec, is_dst = p6, p5, p4, p3, p2, p1, 0, is_dst ? 1 : 0
30+
year, month, mday, hour, min, sec, usec, is_dst = p6, p5, p4, p3, p2, p1, 0, is_dst
3131
end
3232

33+
nsec = nil
34+
if Primitive.is_a?(usec, String)
35+
nsec = usec.to_i * 1000
36+
elsif usec
37+
nsec = (usec * 1000).to_i
38+
end
39+
40+
compose(time_class, utc_offset, year, month, mday, hour, min, sec, nsec, is_dst)
41+
end
42+
43+
def self.compose(time_class, utc_offset, year, month, mday, hour, min, sec, nsec, is_dst)
3344
if Primitive.is_a?(month, String) or month.respond_to?(:to_str)
3445
month = StringValue(month)
3546
month = MonthValue[month.upcase] || month.to_i
@@ -50,23 +61,16 @@ def self.compose(time_class, utc_offset, p1, p2 = nil, p3 = nil, p4 = nil, p5 =
5061
hour = Primitive.rb_num2int(hour || 0)
5162
min = Primitive.rb_num2int(min || 0)
5263

53-
nsec = nil
54-
if Primitive.is_a?(usec, String)
55-
nsec = usec.to_i * 1000
56-
elsif usec
57-
nsec = (usec * 1000).to_i
58-
end
59-
6064
case utc_offset
6165
when :utc
62-
is_dst = -1
66+
is_dst = nil
6367
is_utc = true
6468
utc_offset = nil
6569
when :local
6670
is_utc = false
6771
utc_offset = nil
6872
else
69-
is_dst = -1
73+
is_dst = nil
7074
is_utc = false
7175
end
7276

@@ -88,10 +92,12 @@ def self.compose(time_class, utc_offset, p1, p2 = nil, p3 = nil, p4 = nil, p5 =
8892
end
8993

9094
nsec ||= 0
95+
dst_code = is_dst ? 1 : (Primitive.nil?(is_dst) ? -1 : 0)
9196

92-
Primitive.time_s_from_array(time_class, sec, min, hour, mday, month, year, nsec, is_dst, is_utc, utc_offset)
97+
Primitive.time_s_from_array(time_class, sec, min, hour, mday, month, year, nsec, dst_code, is_utc, utc_offset)
9398
end
9499

100+
# MRI: time_init_parse()
95101
def self.new_from_string(time_class, str, **options)
96102
raise ArgumentError, 'time string should have ASCII compatible encoding' unless str.encoding.ascii_compatible?
97103

@@ -103,10 +109,13 @@ def self.new_from_string(time_class, str, **options)
103109
[ T] (?<hour> \d{2})
104110
: (?<min> \d{2})
105111
: (?<sec> \d{2})
106-
(?:\. (?<usec> \d+) )?
112+
(?:\. (?<subsec> \d+) )?
107113
(?:\s* (?<offset>\S+))?
108114
)?\z/x =~ str
109-
return self.compose(time_class, self.utc_offset_for_compose(offset || options[:in]), year, month, mday, hour, min, sec, usec)
115+
116+
nsec = subsec[0...9].ljust(9, '0').to_i if subsec
117+
utc_offset = self.utc_offset_for_compose(offset || options[:in])
118+
return self.compose(time_class, utc_offset, year, month, mday, hour, min, sec, nsec, nil)
110119
end
111120

112121
raise ArgumentError, "can't parse: #{str.inspect}"

0 commit comments

Comments
 (0)