diff --git a/include/natalie/string_object.hpp b/include/natalie/string_object.hpp index 2733211231..9e3028b89c 100644 --- a/include/natalie/string_object.hpp +++ b/include/natalie/string_object.hpp @@ -152,6 +152,7 @@ class StringObject : public Object { bool eq(Env *, Value arg); Value eqtilde(Env *, Value); Value force_encoding(Env *, Value); + bool include(Env *, Value); Value ljust(Env *, Value, Value); Value lstrip(Env *) const; Value lstrip_in_place(Env *); diff --git a/lib/natalie/compiler/binding_gen.rb b/lib/natalie/compiler/binding_gen.rb index e37bf146d6..c788c6ace9 100644 --- a/lib/natalie/compiler/binding_gen.rb +++ b/lib/natalie/compiler/binding_gen.rb @@ -796,6 +796,7 @@ def generate_name gen.binding('String', 'eql?', 'StringObject', 'eql', argc: 1, pass_env: false, pass_block: false, return_type: :bool) gen.binding('String', 'force_encoding', 'StringObject', 'force_encoding', argc: 1, pass_env: true, pass_block: false, return_type: :Object) gen.binding('String', 'gsub', 'StringObject', 'gsub', argc: 1..2, pass_env: true, pass_block: true, return_type: :Object) +gen.binding('String', 'include?', 'StringObject', 'include', argc: 1, pass_env: true, pass_block: false, return_type: :bool) gen.binding('String', 'index', 'StringObject', 'index', argc: 1, pass_env: true, pass_block: false, return_type: :Object) gen.binding('String', 'initialize', 'StringObject', 'initialize', argc: 0..1, pass_env: true, pass_block: false, return_type: :Object) gen.binding('String', 'inspect', 'StringObject', 'inspect', argc: 0, pass_env: true, pass_block: false, return_type: :Object) diff --git a/spec/core/string/include_spec.rb b/spec/core/string/include_spec.rb new file mode 100644 index 0000000000..407c810804 --- /dev/null +++ b/spec/core/string/include_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#include? with String" do + it "returns true if self contains other_str" do + "hello".include?("lo").should == true + "hello".include?("ol").should == false + end + + it "ignores subclass differences" do + "hello".include?(StringSpecs::MyString.new("lo")).should == true + StringSpecs::MyString.new("hello").include?("lo").should == true + StringSpecs::MyString.new("hello").include?(StringSpecs::MyString.new("lo")).should == true + end + + it "tries to convert other to string using to_str" do + other = mock('lo') + other.should_receive(:to_str).and_return("lo") + + "hello".include?(other).should == true + end + + it "raises a TypeError if other can't be converted to string" do + -> { "hello".include?([]) }.should raise_error(TypeError) + -> { "hello".include?('h'.ord) }.should raise_error(TypeError) + -> { "hello".include?(mock('x')) }.should raise_error(TypeError) + end + + # NATFIXME: Implement EUC-JP encoding + xit "raises an Encoding::CompatibilityError if the encodings are incompatible" do + pat = "ア".encode Encoding::EUC_JP + -> do + "あれ".include?(pat) + end.should raise_error(Encoding::CompatibilityError) + end +end diff --git a/src/string_object.cpp b/src/string_object.cpp index 7534c2bbdd..85d38408a2 100644 --- a/src/string_object.cpp +++ b/src/string_object.cpp @@ -657,6 +657,14 @@ Value StringObject::split(Env *env, Value splitter, Value max_count_value) { } } +bool StringObject::include(Env *env, Value arg) { + auto to_str = "to_str"_s; + if (!arg->is_string() && arg->respond_to(env, to_str)) + arg = arg->send(env, to_str); + arg->assert_type(env, Object::Type::String, "String"); + return ::strstr(c_str(), arg->as_string()->c_str()) != nullptr; +} + Value StringObject::ljust(Env *env, Value length_obj, Value pad_obj) { length_obj->assert_type(env, Object::Type::Integer, "Integer"); size_t length = length_obj->as_integer()->to_nat_int_t() < 0 ? 0 : length_obj->as_integer()->to_nat_int_t();