Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Src/IronRuby.Tests/RubyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,7 @@ public Tests(Driver/*!*/ driver) {
Method_OptionalAndKeywordParameters2,
Range_MinMaxInclude1,
Integer_Power1,
Rational_CSharp1,
Method_LightFrame1,
Method_LightFrame2,
Hash_SelfKey1,
Expand Down
75 changes: 75 additions & 0 deletions Src/IronRuby.Tests/Runtime/MiscTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3171,6 +3171,81 @@ p r
");
}

/// <summary>
/// Rational as a C# type (RubyRational) with its arithmetic, comparison, #to_f and #hash in C#, and
/// Kernel#Rational: Integer, Float, Rational and big operands, coercion, division by zero, NaN,
/// Marshal, hash keys, the conversions and errors left to rational.rb. The expected output is CRuby's.
/// </summary>
public void Rational_CSharp1() {
TestOutput(@"
require 'ruby4'
def t(l); r = begin; yield; rescue => e; [e.class, e.message]; end; puts ""#{l}: #{r.inspect}""; end
a = Rational(1, 3); b = Rational(-2, 5); big = Rational(2**70, 3); c = Rational(5)
t('add') { [a + b, a + 1, a + 2**70, a + 0.5, a + big, big + big, a - b, a - 3, b - a, c - c] }
t('mul') { [a * b, a * 3, a * 0, a * 0.5, big * big, b * -1, a * Rational(3, 1)] }
t('div') { [a / b, a / 3, a / 0.5, a.quo(b), Rational(1) / a, big / a, a / -2, a / Rational(-1, 2)] }
t('div0') { a / 0 }
t('div0r') { a / Rational(0) }
t('div0f') { a / 0.0 }
t('cmp') { [a <=> b, a <=> 1, a <=> a, a <=> 0.25, a <=> Float::NAN, a <=> ""x"", big <=> a, c <=> 5] }
t('eq') { [a == Rational(2, 6), a == b, c == 5, Rational(0) == 0, a == 1.0/3, a == ""x"", Rational(0) == 0.0, big == big] }
t('to_f') { [a.to_f, b.to_f, big.to_f, Rational(10**400, 3*10**399).to_f, Rational(1, 10**400).to_f] }
t('hash') { [a.hash == Rational(2, 6).hash, {a => 1}[Rational(1, 3)], {big => 2}[Rational(2**70, 3)], a.eql?(Rational(1, 3)), a.hash == b.hash] }
o = Object.new; def o.coerce(x) = [x, Rational(1, 2)]
t('coerce') { [a + o, a * o, a <=> o] }
t('bad') { a + ""x"" }
t('int ops') { [1 + a, 2 * a, 3 - a, 4 / a, 1.5 + a, 2**70 + a, 1 <=> a, 1 == Rational(1)] }
t('misc') { [a.numerator, a.denominator, -a, a.abs, a ** 2, a.round, a.floor, a.ceil, a.truncate, a.to_s, a.inspect, a.to_i, a.zero?, a.positive?] }
t('marshal') { [Marshal.load(Marshal.dump(a)), Marshal.load(Marshal.dump(big)), a.instance_variables, a.frozen?] }
t('sum') { (1..10).sum { |i| Rational(1, i) } }
t('ints') { [Rational(1, 3), Rational(2, 4), Rational(-2, -4), Rational(2, -4), Rational(0, 5), Rational(5), Rational(-5), Rational(2**70, 2**68), Rational(6, 2**70)] }
t('zero') { Rational(1, 0) }
t('rat') { [Rational(Rational(1, 2)), Rational(Rational(1, 2), 3), Rational(Rational(1, 2), 1), Rational(1, Rational(1, 2))] }
t('str') { [Rational(""1/3""), Rational(""0.75""), Rational(""x"", exception: false), Rational(1.5), Rational(nil, exception: false)] }
t('nil') { Rational(nil) }
t('nil2') { Rational(1, nil) }
t('arity0') { Rational() }
t('arity3') { Rational(1, 2, 3) }
t('arity4') { Rational(1, 2, 3, 4) }
t('kw') { [Rational(1, 2, exception: false), Rational(""1/2"", exception: true)] }
t('badkw') { Rational(1, exception: 1) }
t('Kernel.') { Kernel.Rational(3, 9) }
t('arity') { method(:Rational).arity }
t('class') { [Rational(1, 3).class, Rational(4, 2).denominator.class] }
", @"
add: [(-1/15), (4/3), (3541774862152233910273/3), 0.8333333333333333, (1180591620717411303425/3), (2361183241434822606848/3), (11/15), (-8/3), (-11/15), (0/1)]
mul: [(-2/15), (1/1), (0/1), 0.16666666666666666, (1393796574908163946345982392040522594123776/9), (2/5), (1/1)]
div: [(-5/6), (1/9), 0.6666666666666666, (-5/6), (3/1), (1180591620717411303424/1), (-1/6), (-2/3)]
div0: [ZeroDivisionError, ""divided by 0""]
div0r: [ZeroDivisionError, ""divided by 0""]
div0f: Infinity
cmp: [1, -1, 0, 1, nil, nil, 1, 0]
eq: [true, false, true, true, true, false, true, true]
to_f: [0.3333333333333333, -0.4, 3.935305402391371e+20, 3.3333333333333335, 0.0]
hash: [true, 1, 2, true, false]
coerce: [(5/6), (1/6), -1]
bad: [TypeError, ""String can't be coerced into Rational""]
int ops: [(4/3), (2/3), (8/3), (12/1), 1.8333333333333333, (3541774862152233910273/3), 1, true]
misc: [1, 3, (-1/3), (1/3), (1/9), 0, 0, 1, 0, ""1/3"", ""(1/3)"", 0, false, true]
marshal: [(1/3), (1180591620717411303424/3), [], true]
sum: (7381/2520)
ints: [(1/3), (1/2), (1/2), (-1/2), (0/1), (5/1), (-5/1), (4/1), (3/590295810358705651712)]
zero: [ZeroDivisionError, ""divided by 0""]
rat: [(1/2), (1/6), (1/2), (2/1)]
str: [(1/3), (3/4), nil, (3/2), nil]
nil: [TypeError, ""can't convert nil into Rational""]
nil2: [TypeError, ""can't convert nil into Rational""]
arity0: [ArgumentError, ""wrong number of arguments (given 0, expected 1..2)""]
arity3: [ArgumentError, ""wrong number of arguments (given 3, expected 1..2)""]
arity4: [ArgumentError, ""wrong number of arguments (given 4, expected 1..2)""]
kw: [(1/2), (1/2)]
badkw: [ArgumentError, ""expected true or false as exception: 1""]
Kernel.: (1/3)
arity: -1
class: [Rational, Integer]
");
}

/// <summary>
/// Integer#** in C# with what it hands rational.rb (__ir_pow_slow__): a negative exponent (a Rational),
/// a negative base to a fractional power (a Complex), coercion, and MRI's answers for 0 ** NaN and
Expand Down
66 changes: 58 additions & 8 deletions Src/Libraries/Builtins/KernelOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -625,18 +625,72 @@ private static bool TryParseRubyInteger(string/*!*/ str, int requestedRadix, out
return site.Target(site, scope.RubyContext.KernelModule, numerator, denominator);
}

// Kernel#Rational(x[, y], exception: true) (MRI's nurat_f_rational): Integers, and a Rational
// over an Integer, are answered here; anything else - Strings, Floats, objects with #to_r,
// the exception: keyword - is rational.rb's __ir_Rational_slow__. Its Ruby `*args,
// exception:' signature put every call on the slow path: Rational(1, 3) cost ~0.55 us.

[RubyMethod("Rational", RubyMethodAttributes.PrivateInstance)]
[RubyMethod("Rational", RubyMethodAttributes.PublicSingleton)]
public static object ToRational(CallSiteStorage<Func<CallSite, object, object, object>>/*!*/ slow, RubyContext/*!*/ context,
object self, object value) {
RubyClass cls;
if (RubyRational.IsIntegerValue(value) && (cls = GetRationalClass(context)) != null) {
return new RubyRational(cls, value, ScriptingRuntimeHelpers.Int32ToObject(1));
}
if (value is RubyRational) {
return value;
}
var site = slow.GetCallSite("__ir_Rational_slow__", 1);
return site.Target(site, self, value);
}

[RubyMethod("Rational", RubyMethodAttributes.PrivateInstance)]
[RubyMethod("Rational", RubyMethodAttributes.PublicSingleton)]
public static object ToRational(CallSiteStorage<Func<CallSite, object, object, object, object>>/*!*/ slow, RubyContext/*!*/ context,
object self, object numerator, object denominator) {
RubyClass cls;
if (RubyRational.IsIntegerValue(numerator) && RubyRational.IsIntegerValue(denominator) &&
(cls = GetRationalClass(context)) != null) {
return RubyRational.Canonicalize(cls, numerator, denominator);
}
var site = slow.GetCallSite("__ir_Rational_slow__", 2);
return site.Target(site, self, numerator, denominator);
}

[RubyMethod("Rational", RubyMethodAttributes.PrivateInstance)]
[RubyMethod("Rational", RubyMethodAttributes.PublicSingleton)]
public static object ToRational(CallSiteStorage<Func<CallSite, object, object, object, object, object>>/*!*/ slow,
object self, object numerator, object denominator, object options) {
var site = slow.GetCallSite("__ir_Rational_slow__", 3);
return site.Target(site, self, numerator, denominator, options);
}

[RubyMethod("Rational", RubyMethodAttributes.PrivateInstance)]
[RubyMethod("Rational", RubyMethodAttributes.PublicSingleton)]
public static object ToRational(object self, object a, object b, object c, object d, params object[]/*!*/ more) {
throw ConversionArity(4 + more.Length, "1..2");
}

// MRI's Rational is a C function of any arity (-1), which reports a missing argument itself
[RubyMethod("Rational", RubyMethodAttributes.PrivateInstance)]
[RubyMethod("Rational", RubyMethodAttributes.PublicSingleton)]
public static object ToRational(object self) {
throw ConversionArity(0, "1..2");
}

// The Rational class of the context that last asked (one per process in practice).
private static RubyClass _rationalClass;

private static RubyClass GetRationalClass(RubyContext/*!*/ context) {
internal static RubyClass GetRationalClass(RubyContext/*!*/ context) {
RubyClass cls = _rationalClass;
if (cls != null && cls.Context == context) {
return cls;
}
object value;
cls = context.ObjectClass.TryGetConstant(null, "Rational", out value) ? value as RubyClass : null;
// rational.rb's Rational: a Ruby subclass of Numeric whose instances are plain Numerics
if (cls == null || cls.IsSingletonClass || cls.Name != "Rational" || cls.GetUnderlyingSystemType() != typeof(Numeric)) {
// the Rational class, whose instances are RubyRationals
if (cls == null || cls.IsSingletonClass || cls.Name != "Rational" || cls.GetUnderlyingSystemType() != typeof(RubyRational)) {
return null;
}
_rationalClass = cls;
Expand All @@ -655,11 +709,7 @@ internal static object CreateCanonicalRational(RubyContext/*!*/ context, BigInte
if (cls == null) {
return null;
}
var result = new Numeric(cls);
context.SetInstanceVariable(result, "@numerator", Protocols.Normalize(numerator));
context.SetInstanceVariable(result, "@denominator", Protocols.Normalize(denominator));
result.Freeze();
return result;
return new RubyRational(cls, Protocols.Normalize(numerator), Protocols.Normalize(denominator));
}

#endregion
Expand Down
Loading
Loading