Skip to content

Commit

Permalink
* range.c: Fix Range#bsearch for floats [Bug ruby#7724]
Browse files Browse the repository at this point in the history
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@38978 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
  • Loading branch information
marcandre committed Jan 29, 2013
1 parent 996ff13 commit 115a51f
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 159 deletions.
230 changes: 71 additions & 159 deletions range.c
Expand Up @@ -471,6 +471,32 @@ range_step(int argc, VALUE *argv, VALUE range)
return range;
}

#if SIZEOF_DOUBLE == 8 && defined(HAVE_INT64_T)
union int64_double {
int64_t i;
double d;
};

static VALUE
int64_as_double_to_num(int64_t i) {
union int64_double convert;
if (i < 0) {
convert.i = -i;
return DBL2NUM(-convert.d);
} else {
convert.i = i;
return DBL2NUM(convert.d);
}
}

static int64_t
double_as_int64(double d) {
union int64_double convert;
convert.d = fabs(d);
return d < 0 ? -convert.i : convert.i;
}
#endif

/*
* call-seq:
* rng.bsearch {|obj| block } -> value
Expand Down Expand Up @@ -529,6 +555,20 @@ range_bsearch(VALUE range)
VALUE beg, end;
int smaller, satisfied = 0;

/* Implementation notes:
* Floats are handled by mapping them to 64 bits integers.
* Apart from sign issues, floats and their 64 bits integer have the
* same order, assuming they are represented as exponent followed
* by the mantissa. This is true with or without implicit bit.
*
* Finding the average of two ints needs to be careful about
* potential overflow (since float to long can use 64 bits)
* as well as the fact that -1/2 can be 0 or -1 in C89.
*
* Note that -0.0 is mapped to the same int as 0.0 as we don't want
* (-1...0.0).bsearch to yield -0.0.
*/

#define BSEARCH_CHECK(val) \
do { \
VALUE v = rb_yield(val); \
Expand All @@ -553,175 +593,47 @@ range_bsearch(VALUE range)
} \
} while (0)

#define BSEARCH(conv) \
do { \
if (EXCL(range)) high--; \
org_high = high; \
while (low < high) { \
mid = ((high < 0) == (low < 0)) ? low + ((high - low) / 2) \
: (low < -high) ? -((-1 - low - high)/2 + 1) : (low + high) / 2; \
BSEARCH_CHECK(conv(mid)); \
if (smaller) { \
high = mid; \
} \
else { \
low = mid + 1; \
} \
} \
if (low == org_high) { \
BSEARCH_CHECK(conv(low)); \
if (!smaller) return Qnil; \
} \
if (!satisfied) return Qnil; \
return conv(low); \
} while (0)


beg = RANGE_BEG(range);
end = RANGE_END(range);

if (FIXNUM_P(beg) && FIXNUM_P(end)) {
long low = FIX2LONG(beg);
long high = FIX2LONG(end);
long mid, org_high;
if (EXCL(range)) high--;
org_high = high;

while (low < high) {
mid = low + ((high - low) / 2);
BSEARCH_CHECK(INT2FIX(mid));
if (smaller) {
high = mid;
}
else {
low = mid + 1;
}
}
if (low == org_high) {
BSEARCH_CHECK(INT2FIX(low));
if (!smaller) return Qnil;
}
if (!satisfied) return Qnil;
return INT2FIX(low);
BSEARCH(INT2FIX);
}
#if SIZEOF_DOUBLE == 8 && defined(HAVE_INT64_T)
else if (RB_TYPE_P(beg, T_FLOAT) || RB_TYPE_P(end, T_FLOAT)) {
double low = RFLOAT_VALUE(rb_Float(beg));
double high = RFLOAT_VALUE(rb_Float(end));
double mid, org_high;
int count;
org_high = high;
#ifdef FLT_RADIX
#ifdef DBL_MANT_DIG
#define BSEARCH_MAXCOUNT (((FLT_RADIX) - 1) * (DBL_MANT_DIG + DBL_MAX_EXP) + 100)
#else
#define BSEARCH_MAXCOUNT (53 + 1023 + 100)
#endif
#else
#define BSEARCH_MAXCOUNT (53 + 1023 + 100)
#endif
if (isinf(high) && high > 0) {
/* the range is (low..INFINITY) */
double nhigh = 1.0, inc;
if (nhigh < low) nhigh = low;
count = BSEARCH_MAXCOUNT;
/* find upper bound by checking low, low*2, low*4, ... */
while (count >= 0 && !isinf(nhigh)) {
BSEARCH_CHECK(DBL2NUM(nhigh));
if (smaller) break;
high = nhigh;
nhigh *= 2;
count--;
}
if (isinf(nhigh) || count < 0) {
/* upper bound not found; then, search in very near INFINITY */
/* (x..INFINITY where x is not INFINITY but x*2 is INFINITY) */
inc = high / 2;
count = BSEARCH_MAXCOUNT;
while (count >= 0 && inc > 0) {
nhigh = high + inc;
if (!isinf(nhigh)) {
BSEARCH_CHECK(DBL2NUM(nhigh));
if (smaller) {
/* upper bound found; */
/* the desired value is within high..nhigh */
low = high;
high = nhigh;
goto binsearch;
}
else {
high = nhigh;
}
}
inc /= 2;
count--;
}
/* lower bound not found; */
/* there is no candidate except INFINITY itself */
high *= 2; /* generate INFINITY */
if (isinf(high) && !EXCL(range)) {
BSEARCH_CHECK(DBL2NUM(high));
if (!satisfied) return Qnil;
if (smaller) return DBL2NUM(high);
}
return Qnil;
}
/* upper bound found; the desired value is within low..nhigh */
high = nhigh;
}
if (isinf(low) && low < 0) {
/* the range is (-INFINITY..high) */
volatile double nlow = -1.0, dec;
if (nlow > high) nlow = high;
count = BSEARCH_MAXCOUNT;
/* find lower bound by checking low, low*2, low*4, ... */
while (count >= 0 && !isinf(nlow)) {
BSEARCH_CHECK(DBL2NUM(nlow));
if (!smaller) break;
low = nlow;
nlow *= 2;
count--;
}
if (isinf(nlow) || count < 0) {
/* lower bound not found; then, search in very near -INFINITY */
/* (-INFINITY..x where x is not -INFINITY but x*2 is -INFINITY) */
dec = low / 2;
count = BSEARCH_MAXCOUNT;
while (count >= 0 && dec < 0) {
nlow = low + dec;
if (!isinf(nlow)) {
BSEARCH_CHECK(DBL2NUM(nlow));
if (!smaller) {
/* lower bound found; */
/* the desired value is within nlow..low */
high = low;
low = nlow;
goto binsearch;
}
else {
low = nlow;
}
}
dec /= 2;
count--;
}
/* lower bound not found; */
/* there is no candidate except -INFINITY itself */
nlow = low * 2; /* generate -INFINITY */
if (isinf(nlow)) {
BSEARCH_CHECK(DBL2NUM(nlow));
if (!satisfied) return Qnil;
if (smaller) return DBL2NUM(nlow);
}
if (!satisfied) return Qnil;
return DBL2NUM(low);
}
low = nlow;
}

binsearch:
/* find the desired value within low..high */
/* where low is not -INFINITY and high is not INFINITY */
count = BSEARCH_MAXCOUNT;
while (low < high && count >= 0) {
mid = low + ((high - low) / 2);
BSEARCH_CHECK(DBL2NUM(mid));
if (smaller) {
high = mid;
}
else {
low = mid;
}
count--;
}
BSEARCH_CHECK(DBL2NUM(low));
if (!smaller) {
BSEARCH_CHECK(DBL2NUM(high));
if (!smaller) {
return Qnil;
}
low = high;
}
if (!satisfied) return Qnil;
if (EXCL(range) && low >= org_high) return Qnil;
return DBL2NUM(low);
#undef BSEARCH_MAXCOUNT
int64_t low = double_as_int64(RFLOAT_VALUE(rb_Float(beg)));
int64_t high = double_as_int64(RFLOAT_VALUE(rb_Float(end)));
int64_t mid, org_high;
BSEARCH(int64_as_double_to_num);
}
#endif
else if (!NIL_P(rb_check_to_integer(beg, "to_int")) &&
!NIL_P(rb_check_to_integer(end, "to_int"))) {
VALUE low = beg;
Expand Down
76 changes: 76 additions & 0 deletions test/ruby/test_range.rb
Expand Up @@ -422,6 +422,82 @@ def test_bsearch_for_float
assert_in_delta(7.0, (0.0..10).bsearch {|x| 7.0 - x })
end

def check_bsearch_values(range, search)
from, to = range.begin, range.end
cmp = range.exclude_end? ? :< : :<=

# (0) trivial test
r = Range.new(to, from, range.exclude_end?).bsearch do |x|
fail "#{to}, #{from}, #{range.exclude_end?}, #{x}"
end
assert_equal nil, r

r = (to...to).bsearch do
fail
end
assert_equal nil, r

# prepare for others
yielded = []
r = range.bsearch do |val|
yielded << val
val >= search
end

# (1) log test
max = case from
when Float then 65
when Integer then Math.log(to-from+(range.exclude_end? ? 0 : 1), 2).to_i + 1
end
assert yielded.size <= max

# (2) coverage test
expect = if search < from
from
elsif search.send(cmp, to)
search
else
nil
end
assert_equal expect, r

# (3) uniqueness test
assert_equal nil, yielded.uniq!

# (4) end of range test
case
when range.exclude_end?
assert !yielded.include?(to)
assert r != to
when search >= to
assert yielded.include?(to)
assert_equal search == to ? to : nil, r
end

# start of range test
if search <= from
assert yielded.include?(from)
assert_equal from, r
end

# (5) out of range test
yielded.each do |val|
assert from <= val && val.send(cmp, to)
end
end

def test_range_bsearch_for_floats
ints = [-1 << 100, -123456789, -42, -1, 0, 1, 42, 123456789, 1 << 100]
floats = [-Float::INFINITY, -Float::MAX, -42.0, -4.2, -Float::EPSILON, -Float::MIN, 0.0, Float::MIN, Float::EPSILON, Math::PI, 4.2, 42.0, Float::MAX, Float::INFINITY]

[ints, floats].each do |values|
values.combination(2).to_a.product(values).each do |(from, to), search|
check_bsearch_values(from..to, search)
check_bsearch_values(from...to, search)
end
end
end

def test_bsearch_for_bignum
bignum = 2**100
ary = [3, 4, 7, 9, 12]
Expand Down

0 comments on commit 115a51f

Please sign in to comment.