Skip to content

Commit

Permalink
[GR-19220] Support Time.new with string timestamp argument and error …
Browse files Browse the repository at this point in the history
…when invalid (#3702)

PullRequest: truffleruby/4389
  • Loading branch information
eregon committed Nov 5, 2024
2 parents 11986d0 + 9ec446e commit 3cfc5c9
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Compatibility:
* Set `RbConfig::CONFIG['archincludedir']` (#3396, @andrykonchin).
* Support the index/length arguments for the string argument to `String#bytesplice` added in 3.3 (#3656, @rwstauner).
* Implement `rb_str_strlen()` (#3697, @Th3-M4jor).
* Support `Time.new` with String argument and error when invalid (#3693, @rwstauner).

Performance:

Expand Down
78 changes: 55 additions & 23 deletions spec/ruby/core/time/new_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,8 @@ def zone.local_to_utc(t)
Time.new("2020-12-25 00:56:17 +0900").should == t
Time.new("2020-12-25 00:57:47 +090130").should == t
Time.new("2020-12-25T00:56:17+09:00").should == t

Time.new("2020-12-25T00:56:17.123456+09:00").should == Time.utc(2020, 12, 24, 15, 56, 17, 123456)
end

it "accepts precision keyword argument and truncates specified digits of sub-second part" do
Expand All @@ -511,6 +513,16 @@ def zone.local_to_utc(t)
Time.new("2021-12-25 00:00:00", in: "-01:00").to_s.should == "2021-12-25 00:00:00 -0100"
end

it "returns Time of Jan 1 for string with just year" do
Time.new("2021").should == Time.new(2021, 1, 1)
Time.new("2021").zone.should == Time.new(2021, 1, 1).zone
Time.new("2021").utc_offset.should == Time.new(2021, 1, 1).utc_offset
end

it "returns Time of Jan 1 for string with just year in timezone specified with in keyword argument" do
Time.new("2021", in: "+17:00").to_s.should == "2021-01-01 00:00:00 +1700"
end

it "converts precision keyword argument into Integer if is not nil" do
obj = Object.new
def obj.to_int; 3; end
Expand Down Expand Up @@ -539,108 +551,128 @@ def obj.to_int; 3; end
it "raises ArgumentError if part of time string is missing" do
-> {
Time.new("2020-12-25 00:56 +09:00")
}.should raise_error(ArgumentError, "missing sec part: 00:56 ")
}.should raise_error(ArgumentError, /missing sec part: 00:56 |can't parse:/)

-> {
Time.new("2020-12-25 00 +09:00")
}.should raise_error(ArgumentError, "missing min part: 00 ")
}.should raise_error(ArgumentError, /missing min part: 00 |can't parse:/)
end

ruby_version_is "3.2.3" do
it "raises ArgumentError if the time part is missing" do
-> {
Time.new("2020-12-25")
}.should raise_error(ArgumentError, /no time information|can't parse:/)
end
end

it "raises ArgumentError if subsecond is missing after dot" do
-> {
Time.new("2020-12-25 00:56:17. +0900")
}.should raise_error(ArgumentError, "subsecond expected after dot: 00:56:17. ")
}.should raise_error(ArgumentError, /subsecond expected after dot: 00:56:17. |can't parse:/)
end

it "raises ArgumentError if String argument is not in the supported format" do
-> {
Time.new("021-12-25 00:00:00.123456 +09:00")
}.should raise_error(ArgumentError, "year must be 4 or more digits: 021")
}.should raise_error(ArgumentError, /year must be 4 or more digits: 021|can't parse:/)

-> {
Time.new("2020-012-25 00:56:17 +0900")
}.should raise_error(ArgumentError, /\Atwo digits mon is expected after [`']-': -012-25 00:\z/)
}.should raise_error(ArgumentError, /\Atwo digits mon is expected after [`']-': -012-25 00:\z|can't parse:/)

-> {
Time.new("2020-2-25 00:56:17 +0900")
}.should raise_error(ArgumentError, /\Atwo digits mon is expected after [`']-': -2-25 00:56\z/)
}.should raise_error(ArgumentError, /\Atwo digits mon is expected after [`']-': -2-25 00:56\z|can't parse:/)

-> {
Time.new("2020-12-215 00:56:17 +0900")
}.should raise_error(ArgumentError, /\Atwo digits mday is expected after [`']-': -215 00:56:\z/)
}.should raise_error(ArgumentError, /\Atwo digits mday is expected after [`']-': -215 00:56:\z|can't parse:/)

-> {
Time.new("2020-12-25 000:56:17 +0900")
}.should raise_error(ArgumentError, "two digits hour is expected: 000:56:17 ")
}.should raise_error(ArgumentError, /two digits hour is expected: 000:56:17 |can't parse:/)

-> {
Time.new("2020-12-25 0:56:17 +0900")
}.should raise_error(ArgumentError, "two digits hour is expected: 0:56:17 +0")
}.should raise_error(ArgumentError, /two digits hour is expected: 0:56:17 \+0|can't parse:/)

-> {
Time.new("2020-12-25 00:516:17 +0900")
}.should raise_error(ArgumentError, /\Atwo digits min is expected after [`']:': :516:17 \+09\z/)
}.should raise_error(ArgumentError, /\Atwo digits min is expected after [`']:': :516:17 \+09\z|can't parse:/)

-> {
Time.new("2020-12-25 00:6:17 +0900")
}.should raise_error(ArgumentError, /\Atwo digits min is expected after [`']:': :6:17 \+0900\z/)
}.should raise_error(ArgumentError, /\Atwo digits min is expected after [`']:': :6:17 \+0900\z|can't parse:/)

-> {
Time.new("2020-12-25 00:56:137 +0900")
}.should raise_error(ArgumentError, /\Atwo digits sec is expected after [`']:': :137 \+0900\z/)
}.should raise_error(ArgumentError, /\Atwo digits sec is expected after [`']:': :137 \+0900\z|can't parse:/)

-> {
Time.new("2020-12-25 00:56:7 +0900")
}.should raise_error(ArgumentError, /\Atwo digits sec is expected after [`']:': :7 \+0900\z/)
}.should raise_error(ArgumentError, /\Atwo digits sec is expected after [`']:': :7 \+0900\z|can't parse:/)

-> {
Time.new("2020-12-25 00:56. +0900")
}.should raise_error(ArgumentError, "fraction min is not supported: 00:56.")
}.should raise_error(ArgumentError, /fraction min is not supported: 00:56\.|can't parse:/)

-> {
Time.new("2020-12-25 00. +0900")
}.should raise_error(ArgumentError, "fraction hour is not supported: 00.")
}.should raise_error(ArgumentError, /fraction hour is not supported: 00\.|can't parse:/)
end

it "raises ArgumentError if date/time parts values are not valid" do
-> {
Time.new("2020-13-25 00:56:17 +09:00")
}.should raise_error(ArgumentError, "mon out of range")
}.should raise_error(ArgumentError, /(mon|argument) out of range/)

-> {
Time.new("2020-12-32 00:56:17 +09:00")
}.should raise_error(ArgumentError, "mday out of range")
}.should raise_error(ArgumentError, /(mday|argument) out of range/)

-> {
Time.new("2020-12-25 25:56:17 +09:00")
}.should raise_error(ArgumentError, "hour out of range")
}.should raise_error(ArgumentError, /(hour|argument) out of range/)

-> {
Time.new("2020-12-25 00:61:17 +09:00")
}.should raise_error(ArgumentError, "min out of range")
}.should raise_error(ArgumentError, /(min|argument) out of range/)

-> {
Time.new("2020-12-25 00:56:61 +09:00")
}.should raise_error(ArgumentError, "sec out of range")
}.should raise_error(ArgumentError, /(sec|argument) out of range/)

-> {
Time.new("2020-12-25 00:56:17 +23:59:60")
}.should raise_error(ArgumentError, "utc_offset out of range")
}.should raise_error(ArgumentError, /(utc_offset|argument) out of range/)

-> {
Time.new("2020-12-25 00:56:17 +24:00")
}.should raise_error(ArgumentError, "utc_offset out of range")
}.should raise_error(ArgumentError, /(utc_offset|argument) out of range/)

-> {
Time.new("2020-12-25 00:56:17 +23:61")
}.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +23:61')
}.should raise_error(ArgumentError, /#{Regexp.escape('"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +23:61')}|can't parse:/)
end

it "raises ArgumentError if string has not ascii-compatible encoding" do
-> {
Time.new("2021-11-31 00:00:60 +09:00".encode("utf-32le"))
}.should raise_error(ArgumentError, "time string should have ASCII compatible encoding")
end

it "raises ArgumentError if string doesn't start with year" do
-> {
Time.new("a\nb")
}.should raise_error(ArgumentError, "can't parse: \"a\\nb\"")
end

it "raises ArgumentError if string has extra characters after offset" do
-> {
Time.new("2021-11-31 00:00:59 +09:00 abc")
}.should raise_error(ArgumentError, /can't parse.+ abc/)
end
end
end
end
9 changes: 0 additions & 9 deletions spec/tags/core/time/new_tags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,6 @@ fails:Time.new with a timezone argument subject's class implements .find_timezon
fails:Time.new with a timezone argument subject's class implements .find_timezone method calls .find_timezone to build a time object if passed zone name as a timezone argument
fails:Time.new with a timezone argument subject's class implements .find_timezone method does not call .find_timezone if passed any not string/numeric/timezone timezone argument
fails:Time.new with a timezone argument :in keyword argument could be a timezone object
fails:Time.new with a timezone argument Time.new with a String argument parses an ISO-8601 like format
fails:Time.new with a timezone argument Time.new with a String argument accepts precision keyword argument and truncates specified digits of sub-second part
fails:Time.new with a timezone argument Time.new with a String argument returns Time in timezone specified in the String argument
fails:Time.new with a timezone argument Time.new with a String argument returns Time in timezone specified in the String argument even if the in keyword argument provided
fails:Time.new with a timezone argument Time.new with a String argument returns Time in timezone specified with in keyword argument if timezone isn't provided in the String argument
fails:Time.new with a timezone argument Time.new with a String argument converts precision keyword argument into Integer if is not nil
fails:Time.new with a timezone argument Time.new with a String argument raise TypeError is can't convert precision keyword argument into Integer
fails:Time.new with a timezone argument Time.new with a String argument raises ArgumentError if part of time string is missing
fails:Time.new with a timezone argument Time.new with a String argument raises ArgumentError if subsecond is missing after dot
fails:Time.new with a timezone argument Time.new with a String argument raises ArgumentError if String argument is not in the supported format
fails:Time.new with a timezone argument Time.new with a String argument raises ArgumentError if date/time parts values are not valid
fails:Time.new with a timezone argument Time.new with a String argument raises ArgumentError if string has not ascii-compatible encoding
6 changes: 5 additions & 1 deletion src/main/ruby/truffleruby/core/time.rb
Original file line number Diff line number Diff line change
Expand Up @@ -403,15 +403,19 @@ def at(sec, sub_sec = undefined, unit = undefined, **kwargs)
time
end

def new(year = undefined, month = nil, day = nil, hour = nil, minute = nil, second = nil, utc_offset = nil, **options)
def new(year = undefined, month = undefined, day = nil, hour = nil, minute = nil, second = nil, utc_offset = nil, **options)
if utc_offset && options[:in]
raise ArgumentError, 'timezone argument given as positional and keyword arguments'
end

utc_offset ||= options[:in]
month_undefined = Primitive.undefined?(month)
month = nil if month_undefined

if Primitive.undefined?(year)
utc_offset ? self.now.getlocal(utc_offset) : self.now
elsif Primitive.is_a?(year, String) && month_undefined
Truffle::TimeOperations.new_from_string(self, year, **options)
elsif Primitive.nil? utc_offset
Truffle::TimeOperations.compose(self, :local, year, month, day, hour, minute, second)
elsif utc_offset == :std
Expand Down
30 changes: 30 additions & 0 deletions src/main/ruby/truffleruby/core/truffle/time_operations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,35 @@ def self.compose(time_class, utc_offset, p1, p2 = nil, p3 = nil, p4 = nil, p5 =

Primitive.time_s_from_array(time_class, sec, min, hour, mday, month, year, nsec, is_dst, is_utc, utc_offset)
end

def self.new_from_string(time_class, str, **options)
raise ArgumentError, 'time string should have ASCII compatible encoding' unless str.encoding.ascii_compatible?

# Fast path for well-formed strings.
if /\A (?<year>\d{4,5})
(?:
- (?<month>\d{2})
- (?<mday> \d{2})
[ T] (?<hour> \d{2})
: (?<min> \d{2})
: (?<sec> \d{2})
(?:\. (?<usec> \d+) )?
\s* (?<offset>\S+)?
)?\z/x =~ str
return self.compose(time_class, self.utc_offset_for_compose(offset || options[:in]), year, month, mday, hour, min, sec, usec)
end

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

def self.utc_offset_for_compose(utc_offset)
if Primitive.nil?(utc_offset)
:local
elsif Time.send(:utc_offset_in_utc?, utc_offset)
:utc
else
Truffle::Type.coerce_to_utc_offset(utc_offset)
end
end
end
end

0 comments on commit 3cfc5c9

Please sign in to comment.