From b1d2ec25c3a184fc11ed76fd410657fe1070b21e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?fn=20=E2=8C=83=20=E2=8C=A5?= <70830482+FnControlOption@users.noreply.github.com> Date: Thu, 6 Jan 2022 17:40:58 -0800 Subject: [PATCH] Implement String#include? --- include/natalie/string.hpp | 11 ++++++--- include/natalie/string_object.hpp | 1 + lib/natalie/compiler/binding_gen.rb | 1 + spec/core/string/include_spec.rb | 36 +++++++++++++++++++++++++++++ src/string_object.cpp | 8 +++++++ test/natalie/string_test.rb | 7 ++++++ 6 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 spec/core/string/include_spec.rb diff --git a/include/natalie/string.hpp b/include/natalie/string.hpp index f5d9ffbf3..bb4d66730 100644 --- a/include/natalie/string.hpp +++ b/include/natalie/string.hpp @@ -283,10 +283,15 @@ class String : public Cell { } ssize_t find(const String &needle) const { - const char *index = strstr(m_str, needle.c_str()); - if (index == nullptr) + if (m_length < needle.length()) return -1; - return index - m_str; + size_t max_index = m_length - needle.length(); + size_t byte_count = sizeof(char) * needle.length(); + for (size_t index = 0; index <= max_index; ++index) { + if (memcmp(m_str + index, needle.c_str(), byte_count) == 0) + return index; + } + return -1; } ssize_t find(const char c) const { diff --git a/include/natalie/string_object.hpp b/include/natalie/string_object.hpp index f2da70a73..d287a758c 100644 --- a/include/natalie/string_object.hpp +++ b/include/natalie/string_object.hpp @@ -153,6 +153,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 6d7fbe2f3..248e3336a 100644 --- a/lib/natalie/compiler/binding_gen.rb +++ b/lib/natalie/compiler/binding_gen.rb @@ -797,6 +797,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 000000000..407c81080 --- /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 000fa047c..d45aa1a0a 100644 --- a/src/string_object.cpp +++ b/src/string_object.cpp @@ -667,6 +667,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 m_string.find(arg->as_string()->m_string) != -1; +} + 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(); diff --git a/test/natalie/string_test.rb b/test/natalie/string_test.rb index 64f8c118d..e336ed7b8 100644 --- a/test/natalie/string_test.rb +++ b/test/natalie/string_test.rb @@ -449,4 +449,11 @@ def initialize(s) 'tim'.reverse.should == 'mit' end end + + describe '#include?' do + it 'works on strings containing null character' do + "foo\x00bar".include?('bar').should == true + 'foo'.include?("foo\x00bar").should == false + end + end end