Feature #12732
closedAn option to pass to `Integer`, `Float`, to return `nil` instead of raise an exception
Added by tenderlovemaking (Aaron Patterson) about 8 years ago. Updated almost 3 years ago.
Description
I would like to be able to pass an option to Integer()
and Float()
so that they don't raise an exception, but return nil
instead. For example:
Integer(string, exception: false)
The reason I want this function is so that I can convert strings from YAML or JSON to integers if they parse correctly, or just return strings if they can't be parsed.
Files
integer-parse.pdf (29 KB) integer-parse.pdf | tenderlovemaking (Aaron Patterson), 09/07/2016 06:55 AM |
Updated by tenderlovemaking (Aaron Patterson) about 8 years ago
- File integer-parse.pdf integer-parse.pdf added
Adding a slide to show code I'm actually writing vs want to write
Updated by shyouhei (Shyouhei Urabe) about 8 years ago
We looked at this issue in developer meeting today.
It seems originally, ruby was designed under assumption that string to integer conversion in general could be covered 100% by either to_s or Integer(). Truth is we need the proposed functionality.
People at the meeting was not sure about the API though. Is it a variant of Integer() or a separate new method? For instance an attendee suggested "Integer?()" but could not be popular.
Updated by shyouhei (Shyouhei Urabe) almost 8 years ago
- Related to Feature #12968: Allow default value via block for Integer(), Float() and Rational() added
Updated by matz (Yukihiro Matsumoto) almost 8 years ago
- Status changed from Open to Feedback
- Assignee set to matz (Yukihiro Matsumoto)
Is there any problem with the following code?
Integer(str) rescue default_value
Matz.
Updated by naruse (Yui NARUSE) almost 8 years ago
Below is PoC; it may have a path which raises an exception.
diff --git a/object.c b/object.c
index 05bef4d..5d63803 100644
--- a/object.c
+++ b/object.c
@@ -2750,17 +2750,60 @@ static VALUE
rb_f_integer(int argc, VALUE *argv, VALUE obj)
{
VALUE arg = Qnil;
+ VALUE opts = Qnil;
+ VALUE exception = Qnil;
+ VALUE vbase = Qundef;
int base = 0;
+ static ID int_kwds[1];
- switch (argc) {
- case 2:
- base = NUM2INT(argv[1]);
- case 1:
- arg = argv[0];
- break;
- default:
- /* should cause ArgumentError */
- rb_scan_args(argc, argv, "11", NULL, NULL);
+ rb_scan_args(argc, argv, "11:", &arg, &vbase, &opts);
+ if (!NIL_P(vbase)) {
+ base = NUM2INT(vbase);
+ }
+ if (!NIL_P(opts)) {
+ if (!int_kwds[0]) {
+ int_kwds[0] = rb_intern_const("exception");
+ }
+ if (rb_get_kwargs(opts, int_kwds, 0, 1, &exception)) {
+ VALUE tmp;
+ if (RB_FLOAT_TYPE_P(arg)) {
+ double f;
+ if (base != 0) goto arg_error;
+ f = RFLOAT_VALUE(arg);
+ if (FIXABLE(f)) return LONG2FIX((long)f);
+ return rb_dbl2big(f);
+ }
+ else if (RB_INTEGER_TYPE_P(arg)) {
+ if (base != 0) goto arg_error;
+ return arg;
+ }
+ else if (RB_TYPE_P(arg, T_STRING)) {
+ const char *s;
+ long len;
+ rb_must_asciicompat(arg);
+ RSTRING_GETMEM(arg, s, len);
+ tmp = rb_cstr_parse_inum(s, len, NULL, base);
+ if (NIL_P(tmp)) {
+ return exception;
+ }
+ return tmp;
+ }
+ else if (NIL_P(arg)) {
+ if (base != 0) goto arg_error;
+ return exception;
+ }
+ if (base != 0) {
+ tmp = rb_check_string_type(arg);
+ if (!NIL_P(tmp)) return rb_str_to_inum(tmp, base, TRUE);
+arg_error:
+ rb_raise(rb_eArgError, "base specified for non string value");
+ }
+ tmp = convert_type(arg, "Integer", "to_int", FALSE);
+ if (NIL_P(tmp)) {
+ return rb_to_integer(arg, "to_i");
+ }
+ return tmp;
+ }
}
return rb_convert_to_integer(arg, base);
}
def assert(a, b)
if a != b
raise "'#{a}' != '#{b}'"
end
end
def assert_raise(ex)
begin
yield
raise "#{ex} is expected but not raised"
rescue ex
# correct
rescue
raise "#{ex} is expected but #{$!.inspect}"
end
end
o = Object.new
assert 123, Integer("123")
assert 50, Integer("32", 16)
assert 16, Integer("10", 16, exception: o)
assert o, Integer("x", exception: o)
assert o, Integer("x", 16, exception: o)
assert_raise(ArgumentError){ Integer("x") }
assert_raise(ArgumentError){ Integer("x", 16) }
require'benchmark/ips'
Benchmark.ips{|x|
x.report("rescue") {
Integer('foo') rescue nil
}
x.report("kwarg") {
Integer('foo', exception: nil)
}
}
Warming up --------------------------------------
rescue 36.258k i/100ms
kwarg 64.004k i/100ms
Calculating -------------------------------------
rescue 392.926k (± 8.9%) i/s - 1.958M in 5.025204s
kwarg 844.563k (±14.9%) i/s - 4.096M in 5.017539s
Updated by tenderlovemaking (Aaron Patterson) almost 8 years ago
Hi,
Is there any problem with the following code?
Integer(str) rescue default_value
2 problems
- It's slower than it could be (as Naruse demonstrates)
- It's very noisy when
-d
is enabled.
In Psych, I am trying to avoid noise from -d
. That means I have to try to check if the string will work with Integer()
, then actually call Integer()
. It means the string has to be parsed twice. If Integer(str) rescue default_value
didn't make noise, then I would be OK with that. :)
Updated by rbjl (Jan Lelis) almost 8 years ago
Although it does not solve Aaron's use case, I would suggest to have a Integer.try_convert
, Float.try_convert
, Rational.try_convert
, and Complex.try_convert
which do not raise exceptions, but just return nil
. To keep consistency, they would just call implicit, then explicit conversion (e.g. to_int
, then to_i
), instead of Integer()
's special parsing.
It only allows strict parsing, but is much cleaner, imho. Also, it would fill some empty spots in the core conversion table and make Ruby's conversion logic simpler. (.try_convert
, which currently feels more like an implementation detail, could then be embraced more).
To allow Integer()
special conversion, it would still need an exception:
option, but also Float()
, Rational()
, and Complex()
would need it (since they currently also lack this feature due to not having a try_convert
). One idea is to give every of the uppercased Kernel methods an exception:
option, but this does not make sense for Array()
and just would not be needed if going for the broader try_convert
support).
To summarize my suggestion in two bullet points:
- Create
Integer.try_convert
,Float.try_convert
,Rational.try_convert
, andComplex.try_convert
which prefer implicit conversion (if available), then explicit conversion, but returnnil
instead of raising an exception - Give
Integer()
anexception
option to support special integer parsing without exceptions, but do not giveFloat()
one
Updated by matz (Yukihiro Matsumoto) almost 7 years ago
Aaron's comment in #note-6 sounds reasonable. Accepted.
Matz.
Updated by knu (Akinori MUSHA) almost 7 years ago
Just for the record, Integer(x, rescue: default_value)
might be an idea, if anything other than nil
(like zero) would be desired.
Updated by tenderlovemaking (Aaron Patterson) almost 7 years ago
Something like Integer(x, rescue: default_value)
is fine for me too, (or Integer(x, ->() { default_value })
, which is similar to [].find(->() { missing_value }) { ... }
) Configuring with a default value seems more flexible.
Updated by nobu (Nobuyoshi Nakada) almost 7 years ago
Since Integer()
has radix
optional argument, new optional argument might be confusing.
A keyword argument or a block would be better, I think.
Updated by enebo (Thomas Enebo) almost 7 years ago
Two comments:
- having block form only defeats any performance gain as executing blocks have a measurable cost. It may be nice to have though in addition to simple nil return form.
- it would be really nice if Ruby had some API consistency for non-exception variants of the various callswhich have made this change. Doing each one of these as a one-off discussion almost destines these APIs to not be consistent.
Updated by mrkn (Kenta Murata) over 6 years ago
- Status changed from Feedback to Closed
Applied in changeset ruby-trunk:trunk|r62757.
Add exception:
keyword in Kernel#Integer()
Support exception:
keyword argument in Kernel#Integer().
If exception:
is false
, Kernel#Integer()
returns nil
if the given
value cannot be interpreted as an integer value.
The default value of exception:
is true
.
This is part of [Feature #12732].
Updated by m_s__santos (Matheus Silva) over 6 years ago
rbjl (Jan Lelis) wrote:
Although it does not solve Aaron's use case, I would suggest to have a
Integer.try_convert
,Float.try_convert
,Rational.try_convert
, andComplex.try_convert
which do not raise exceptions, but just returnnil
. To keep consistency, they would just call implicit, then explicit conversion (e.g.to_int
, thento_i
), instead ofInteger()
's special parsing.It only allows strict parsing, but is much cleaner, imho. Also, it would fill some empty spots in the core conversion table and make Ruby's conversion logic simpler. (
.try_convert
, which currently feels more like an implementation detail, could then be embraced more).To allow
Integer()
special conversion, it would still need anexception:
option, but alsoFloat()
,Rational()
, andComplex()
would need it (since they currently also lack this feature due to not having atry_convert
). One idea is to give every of the uppercased Kernel methods anexception:
option, but this does not make sense forArray()
and just would not be needed if going for the broadertry_convert
support).To summarize my suggestion in two bullet points:
- Create
Integer.try_convert
,Float.try_convert
,Rational.try_convert
, andComplex.try_convert
which prefer implicit conversion (if available), then explicit conversion, but returnnil
instead of raising an exception- Give
Integer()
anexception
option to support special integer parsing without exceptions, but do not giveFloat()
one
It would be better if Integer.try_convert
return the conversion or the value passed if it can't convert.
Updated by hsbt (Hiroshi SHIBATA) almost 3 years ago
- Project changed from 14 to Ruby master