Skip to content

Commit

Permalink
Implement String#include?
Browse files Browse the repository at this point in the history
  • Loading branch information
FnControlOption committed Jan 7, 2022
1 parent a00940b commit b1d2ec2
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 3 deletions.
11 changes: 8 additions & 3 deletions include/natalie/string.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions include/natalie/string_object.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 *);
Expand Down
1 change: 1 addition & 0 deletions lib/natalie/compiler/binding_gen.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions spec/core/string/include_spec.rb
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions src/string_object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
7 changes: 7 additions & 0 deletions test/natalie/string_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit b1d2ec2

Please sign in to comment.