diff --git a/include/natalie/object.hpp b/include/natalie/object.hpp index b63733b333..a50000510b 100644 --- a/include/natalie/object.hpp +++ b/include/natalie/object.hpp @@ -279,6 +279,7 @@ class Object : public Cell { ArrayObject *to_ary(Env *env); IntegerObject *to_int(Env *env); + StringObject *to_str(Env *env); protected: ClassObject *m_klass { nullptr }; diff --git a/include/natalie/string_object.hpp b/include/natalie/string_object.hpp index becc5d7431..e9d12b6488 100644 --- a/include/natalie/string_object.hpp +++ b/include/natalie/string_object.hpp @@ -146,7 +146,8 @@ class StringObject : public Object { return this; } - bool start_with(Env *, Value) const; + bool internal_start_with(Env *, Value) const; + bool start_with(Env *, size_t, Value *) const; bool end_with(Env *, Value) const; bool is_empty() const { return m_string.is_empty(); } diff --git a/lib/natalie/compiler/binding_gen.rb b/lib/natalie/compiler/binding_gen.rb index a5e8712cfe..3f748dc760 100644 --- a/lib/natalie/compiler/binding_gen.rb +++ b/lib/natalie/compiler/binding_gen.rb @@ -869,7 +869,7 @@ def generate_name gen.binding('String', 'rstrip!', 'StringObject', 'rstrip_in_place', argc: 0, pass_env: true, pass_block: false, return_type: :Object) gen.binding('String', 'size', 'StringObject', 'size', argc: 0, pass_env: true, pass_block: false, return_type: :Object) gen.binding('String', 'split', 'StringObject', 'split', argc: 0..2, pass_env: true, pass_block: false, return_type: :Object) -gen.binding('String', 'start_with?', 'StringObject', 'start_with', argc: 1, pass_env: true, pass_block: false, return_type: :bool) +gen.binding('String', 'start_with?', 'StringObject', 'start_with', argc: :any, pass_env: true, pass_block: false, return_type: :bool) gen.binding('String', 'strip', 'StringObject', 'strip', argc: 0, pass_env: true, pass_block: false, return_type: :Object) gen.binding('String', 'strip!', 'StringObject', 'strip_in_place', argc: 0, pass_env: true, pass_block: false, return_type: :Object) gen.binding('String', 'sub', 'StringObject', 'sub', argc: 1..2, pass_env: true, pass_block: true, return_type: :Object) diff --git a/spec/core/string/start_with_spec.rb b/spec/core/string/start_with_spec.rb new file mode 100644 index 0000000000..aaed197ff3 --- /dev/null +++ b/spec/core/string/start_with_spec.rb @@ -0,0 +1,8 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/string/start_with' + +describe "String#start_with?" do + it_behaves_like :start_with, :to_s +end diff --git a/spec/shared/string/start_with.rb b/spec/shared/string/start_with.rb new file mode 100644 index 0000000000..d6448a1c45 --- /dev/null +++ b/spec/shared/string/start_with.rb @@ -0,0 +1,75 @@ +describe :start_with, shared: true do + # the @method should either be :to_s or :to_sym + + it "returns true only if beginning match" do + s = "hello".send(@method) + s.should.start_with?('h') + s.should.start_with?('hel') + s.should_not.start_with?('el') + end + + it "returns true only if any beginning match" do + "hello".send(@method).should.start_with?('x', 'y', 'he', 'z') + end + + it "returns true if the search string is empty" do + "hello".send(@method).should.start_with?("") + "".send(@method).should.start_with?("") + end + + it "converts its argument using :to_str" do + s = "hello".send(@method) + find = mock('h') + find.should_receive(:to_str).and_return("h") + s.should.start_with?(find) + end + + it "ignores arguments not convertible to string" do + "hello".send(@method).should_not.start_with?() + -> { "hello".send(@method).start_with?(1) }.should raise_error(TypeError) + -> { "hello".send(@method).start_with?(["h"]) }.should raise_error(TypeError) + -> { "hello".send(@method).start_with?(1, nil, "h") }.should raise_error(TypeError) + end + + it "uses only the needed arguments" do + find = mock('h') + find.should_not_receive(:to_str) + "hello".send(@method).should.start_with?("h",find) + end + + it "works for multibyte strings" do + "céréale".send(@method).should.start_with?("cér") + end + + # NATFIXME: Add support for regexps + xit "supports regexps" do + regexp = /[h1]/ + "hello".send(@method).should.start_with?(regexp) + "1337".send(@method).should.start_with?(regexp) + "foxes are 1337".send(@method).should_not.start_with?(regexp) + "chunky\n12bacon".send(@method).should_not.start_with?(/12/) + end + + # NATFIXME: Add support for regexps + xit "supports regexps with ^ and $ modifiers" do + regexp1 = /^\d{2}/ + regexp2 = /\d{2}$/ + "12test".send(@method).should.start_with?(regexp1) + "test12".send(@method).should_not.start_with?(regexp1) + "12test".send(@method).should_not.start_with?(regexp2) + "test12".send(@method).should_not.start_with?(regexp2) + end + + # NATFIXME: Add support for regexps + xit "sets Regexp.last_match if it returns true" do + regexp = /test-(\d+)/ + "test-1337".send(@method).start_with?(regexp).should be_true + Regexp.last_match.should_not be_nil + Regexp.last_match[1].should == "1337" + $1.should == "1337" + + "test-asdf".send(@method).start_with?(regexp).should be_false + Regexp.last_match.should be_nil + $1.should be_nil + end +end diff --git a/src/object.cpp b/src/object.cpp index a34c863b09..5712d01e26 100644 --- a/src/object.cpp +++ b/src/object.cpp @@ -878,4 +878,24 @@ IntegerObject *Object::to_int(Env *env) { result->klass()->inspect_str()); } +StringObject *Object::to_str(Env *env) { + if (is_string()) return as_string(); + + auto to_str = "to_str"_s; + if (! respond_to(env, to_str)) { + assert_type(env, Type::String, "String"); + } + + auto result = send(env, to_str); + + if (result->is_string()) + return result->as_string(); + + env->raise( + "TypeError", "can't convert {} to String ({}#to_str gives {})", + klass()->inspect_str(), + klass()->inspect_str(), + result->klass()->inspect_str()); +} + } diff --git a/src/string_object.cpp b/src/string_object.cpp index 0170a9cce5..d7c01e182f 100644 --- a/src/string_object.cpp +++ b/src/string_object.cpp @@ -198,11 +198,23 @@ StringObject *StringObject::successive(Env *env) { return new StringObject { str }; } -bool StringObject::start_with(Env *env, Value needle) const { +bool StringObject::internal_start_with(Env *env, Value needle) const { nat_int_t i = index_int(env, needle, 0); + return i == 0; } +bool StringObject::start_with(Env *env, size_t argc, Value *args) const { + for (size_t i = 0; i < argc; ++i) { + auto arg = args[i]; + + if (internal_start_with(env, arg)) + return true; + } + + return false; +} + bool StringObject::end_with(Env *env, Value needle) const { needle->assert_type(env, Object::Type::String, "String"); if (length() < needle->as_string()->length()) @@ -233,8 +245,9 @@ Value StringObject::index(Env *env, Value needle, size_t start) { } nat_int_t StringObject::index_int(Env *env, Value needle, size_t start) const { - needle->assert_type(env, Object::Type::String, "String"); - const char *ptr = strstr(c_str() + start, needle->as_string()->c_str()); + auto needle_str = needle->to_str(env)->as_string()->c_str(); + + const char *ptr = strstr(c_str() + start, needle_str); if (ptr == nullptr) return -1; return ptr - c_str(); diff --git a/src/symbol_object.cpp b/src/symbol_object.cpp index 928253b021..b3b19613d9 100644 --- a/src/symbol_object.cpp +++ b/src/symbol_object.cpp @@ -82,7 +82,7 @@ Value SymbolObject::cmp(Env *env, Value other_value) { } bool SymbolObject::start_with(Env *env, Value needle) { - return to_s(env)->start_with(env, needle); + return to_s(env)->internal_start_with(env, needle); } Value SymbolObject::ref(Env *env, Value index_obj) {