Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Time.new with string timestamp argument and error when invalid #3702

Merged
merged 7 commits into from
Nov 6, 2024
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
Comment on lines +99 to +108
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beautiful :)

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
Loading