diff --git a/include/natalie/string.hpp b/include/natalie/string.hpp index c94db044db..740b26adae 100644 --- a/include/natalie/string.hpp +++ b/include/natalie/string.hpp @@ -168,6 +168,16 @@ class String : public Cell { append(buf); } + void prepend(const String *str) { + if (!str) return; + size_t new_length = str->length(); + if (new_length == 0) return; + char buf[new_length + m_length + 1]; + memcpy(buf, str->m_str, sizeof(char) * new_length); + memcpy(buf + new_length, m_str, sizeof(char) * (m_length + 1)); + set_str(buf); + } + void insert(size_t position, char c) { assert(position < m_length); grow_at_least(m_length + 1); diff --git a/include/natalie/string_object.hpp b/include/natalie/string_object.hpp index dcde7821af..ae07633994 100644 --- a/include/natalie/string_object.hpp +++ b/include/natalie/string_object.hpp @@ -161,6 +161,7 @@ class StringObject : public Object { Value match(Env *, Value); Value mul(Env *, Value) const; Value ord(Env *); + Value prepend(Env *, size_t, Value *); Value ref(Env *, Value); Value reverse(Env *); Value rstrip(Env *) const; diff --git a/lib/natalie/compiler/binding_gen.rb b/lib/natalie/compiler/binding_gen.rb index 2e47ec88a2..30f0de742e 100644 --- a/lib/natalie/compiler/binding_gen.rb +++ b/lib/natalie/compiler/binding_gen.rb @@ -812,6 +812,7 @@ def generate_name gen.binding('String', 'lstrip', 'StringObject', 'lstrip', argc: 0, pass_env: true, pass_block: false, return_type: :Object) gen.binding('String', 'lstrip!', 'StringObject', 'lstrip_in_place', argc: 0, pass_env: true, pass_block: false, return_type: :Object) gen.binding('String', 'match', 'StringObject', 'match', argc: 1, pass_env: true, pass_block: false, return_type: :Object) + gen.binding('String', 'prepend', 'StringObject', 'prepend', argc: :any, pass_env: true, pass_block: false, return_type: :Object) gen.binding('String', 'ord', 'StringObject', 'ord', argc: 0, pass_env: true, pass_block: false, return_type: :Object) gen.binding('String', 'reverse', 'StringObject', 'reverse', argc: 0, pass_env: true, pass_block: false, return_type: :Object) gen.binding('String', 'rstrip', 'StringObject', 'rstrip', argc: 0, pass_env: true, pass_block: false, return_type: :Object) diff --git a/spec/core/string/prepend_spec.rb b/spec/core/string/prepend_spec.rb new file mode 100644 index 0000000000..a6074be3c6 --- /dev/null +++ b/spec/core/string/prepend_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#prepend" do + it "prepends the given argument to self and returns self" do + str = "world" + str.prepend("hello ").should equal(str) + str.should == "hello world" + end + + it "converts the given argument to a String using to_str" do + obj = mock("hello") + obj.should_receive(:to_str).and_return("hello") + a = " world!".prepend(obj) + a.should == "hello world!" + end + + it "raises a TypeError if the given argument can't be converted to a String" do + -> { "hello ".prepend [] }.should raise_error(TypeError) + -> { 'hello '.prepend mock('x') }.should raise_error(TypeError) + end + + it "raises a FrozenError when self is frozen" do + a = "hello" + a.freeze + + -> { a.prepend "" }.should raise_error(FrozenError) + -> { a.prepend "test" }.should raise_error(FrozenError) + end + + it "works when given a subclass instance" do + a = " world" + a.prepend StringSpecs::MyString.new("hello") + a.should == "hello world" + end + + ruby_version_is ''...'2.7' do + it "taints self if other is tainted" do + x = "x" + x.prepend("".taint).tainted?.should be_true + + x = "x" + x.prepend("y".taint).tainted?.should be_true + end + end + + it "takes multiple arguments" do + str = " world" + str.prepend "he", "", "llo" + str.should == "hello world" + end + + it "prepends the initial value when given arguments contain 2 self" do + str = "hello" + str.prepend str, str + str.should == "hellohellohello" + end + + it "returns self when given no arguments" do + str = "hello" + str.prepend.should equal(str) + str.should == "hello" + end +end diff --git a/src/string_object.cpp b/src/string_object.cpp index 078b2012f4..290565981b 100644 --- a/src/string_object.cpp +++ b/src/string_object.cpp @@ -344,6 +344,40 @@ Value StringObject::ord(Env *env) { return Value::integer(code); } +Value StringObject::prepend(Env *env, size_t argc, Value *args) { + assert_not_frozen(env); + + StringObject *original = new StringObject(*this); + + auto to_str = "to_str"_s; + String appendable; + for (size_t i = 0; i < argc; i++) { + auto arg = args[i]; + + if (arg == this) + arg = original; + + StringObject *str_obj; + if (arg->is_string()) { + str_obj = arg->as_string(); + } else if (arg->is_integer() && arg->as_integer()->to_nat_int_t() < 0) { + env->raise("RangeError", "less than 0"); + } else if (arg->is_integer()) { + str_obj = arg.send(env, "chr"_s)->as_string(); + } else if (arg->respond_to(env, to_str)) { + str_obj = arg.send(env, to_str)->as_string(); + } else { + env->raise("TypeError", "cannot call to_str", arg->inspect_str(env)); + } + + str_obj->assert_type(env, Object::Type::String, "String"); + appendable.append(&str_obj->m_string); + } + m_string.prepend(&appendable); + + return this; +} + Value StringObject::bytes(Env *env) const { ArrayObject *ary = new ArrayObject { length() }; for (size_t i = 0; i < length(); i++) {