Skip to content

Commit

Permalink
Merge pull request #518 from ryangjchandler/feature/String#center
Browse files Browse the repository at this point in the history
Make `String#center` spec-compliant
  • Loading branch information
seven1m authored Mar 1, 2022
2 parents 3a16250 + c221fbc commit c87ab28
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 0 deletions.
1 change: 1 addition & 0 deletions include/natalie/string_object.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ class StringObject : public Object {
Value add(Env *, Value) const;
Value b(Env *) const;
Value bytes(Env *, Block *);
Value center(Env *, Value, Value);
Value chr(Env *);
Value cmp(Env *, Value) const;
Value concat(Env *env, size_t argc, Value *args);
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 @@ -838,6 +838,7 @@ def generate_name
gen.binding('String', 'b', 'StringObject', 'b', argc: 0, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('String', 'bytes', 'StringObject', 'bytes', argc: 0, pass_env: true, pass_block: true, return_type: :Object)
gen.binding('String', 'bytesize', 'StringObject', 'bytesize', argc: 0, pass_env: false, pass_block: false, return_type: :size_t)
gen.binding('String', 'center', 'StringObject', 'center', argc: 1..2, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('String', 'chars', 'StringObject', 'chars', argc: 0, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('String', 'chr', 'StringObject', 'chr', argc: 0, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('String', 'concat', 'StringObject', 'concat', argc: :any, pass_env: true, pass_block: false, return_type: :Object)
Expand Down
150 changes: 150 additions & 0 deletions spec/core/string/center_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# -*- encoding: utf-8 -*-
require_relative '../../spec_helper'
require_relative 'fixtures/classes'

describe "String#center with length, padding" do
it "returns a new string of specified length with self centered and padded with padstr" do
"one".center(9, '.').should == "...one..."
"hello".center(20, '123').should == "1231231hello12312312"
"middle".center(13, '-').should == "---middle----"

"".center(1, "abcd").should == "a"
"".center(2, "abcd").should == "aa"
"".center(3, "abcd").should == "aab"
"".center(4, "abcd").should == "abab"
"".center(6, "xy").should == "xyxxyx"
"".center(11, "12345").should == "12345123451"

"|".center(2, "abcd").should == "|a"
"|".center(3, "abcd").should == "a|a"
"|".center(4, "abcd").should == "a|ab"
"|".center(5, "abcd").should == "ab|ab"
"|".center(6, "xy").should == "xy|xyx"
"|".center(7, "xy").should == "xyx|xyx"
"|".center(11, "12345").should == "12345|12345"
"|".center(12, "12345").should == "12345|123451"

"||".center(3, "abcd").should == "||a"
"||".center(4, "abcd").should == "a||a"
"||".center(5, "abcd").should == "a||ab"
"||".center(6, "abcd").should == "ab||ab"
"||".center(8, "xy").should == "xyx||xyx"
"||".center(12, "12345").should == "12345||12345"
"||".center(13, "12345").should == "12345||123451"
end

it "pads with whitespace if no padstr is given" do
"two".center(5).should == " two "
"hello".center(20).should == " hello "
end

it "returns self if it's longer than or as long as the specified length" do
"".center(0).should == ""
"".center(-1).should == ""
"hello".center(4).should == "hello"
"hello".center(-1).should == "hello"
"this".center(3).should == "this"
"radiology".center(8, '-').should == "radiology"
end

ruby_version_is ''...'2.7' do
it "taints result when self or padstr is tainted" do
"x".taint.center(4).should.tainted?
"x".taint.center(0).should.tainted?
"".taint.center(0).should.tainted?
"x".taint.center(4, "*").should.tainted?
"x".center(4, "*".taint).should.tainted?
end
end

it "calls #to_int to convert length to an integer" do
"_".center(3.8, "^").should == "^_^"

obj = mock('3')
obj.should_receive(:to_int).and_return(3)

"_".center(obj, "o").should == "o_o"
end

it "raises a TypeError when length can't be converted to an integer" do
-> { "hello".center("x") }.should raise_error(TypeError)
-> { "hello".center("x", "y") }.should raise_error(TypeError)
-> { "hello".center([]) }.should raise_error(TypeError)
-> { "hello".center(mock('x')) }.should raise_error(TypeError)
end

it "calls #to_str to convert padstr to a String" do
padstr = mock('123')
padstr.should_receive(:to_str).and_return("123")

"hello".center(20, padstr).should == "1231231hello12312312"
end

it "raises a TypeError when padstr can't be converted to a string" do
-> { "hello".center(20, 100) }.should raise_error(TypeError)
-> { "hello".center(20, []) }.should raise_error(TypeError)
-> { "hello".center(20, mock('x')) }.should raise_error(TypeError)
end

it "raises an ArgumentError if padstr is empty" do
-> { "hello".center(10, "") }.should raise_error(ArgumentError)
-> { "hello".center(0, "") }.should raise_error(ArgumentError)
end

ruby_version_is ''...'3.0' do
it "returns subclass instances when called on subclasses" do
StringSpecs::MyString.new("").center(10).should be_an_instance_of(StringSpecs::MyString)
StringSpecs::MyString.new("foo").center(10).should be_an_instance_of(StringSpecs::MyString)
StringSpecs::MyString.new("foo").center(10, StringSpecs::MyString.new("x")).should be_an_instance_of(StringSpecs::MyString)

"".center(10, StringSpecs::MyString.new("x")).should be_an_instance_of(String)
"foo".center(10, StringSpecs::MyString.new("x")).should be_an_instance_of(String)
end
end

ruby_version_is '3.0' do
it "returns String instances when called on subclasses" do
StringSpecs::MyString.new("").center(10).should be_an_instance_of(String)
StringSpecs::MyString.new("foo").center(10).should be_an_instance_of(String)
StringSpecs::MyString.new("foo").center(10, StringSpecs::MyString.new("x")).should be_an_instance_of(String)

"".center(10, StringSpecs::MyString.new("x")).should be_an_instance_of(String)
"foo".center(10, StringSpecs::MyString.new("x")).should be_an_instance_of(String)
end
end

ruby_version_is ''...'2.7' do
it "when padding is tainted and self is untainted returns a tainted string if and only if length is longer than self" do
"hello".center(4, 'X'.taint).tainted?.should be_false
"hello".center(5, 'X'.taint).tainted?.should be_false
"hello".center(6, 'X'.taint).tainted?.should be_true
end
end

# NATFIXME: Add back after adding encodings.
xdescribe "with width" do
it "returns a String in the same encoding as the original" do
str = "abc".force_encoding Encoding::IBM437
result = str.center 6
result.should == " abc "
result.encoding.should equal(Encoding::IBM437)
end
end

# NATFIXME: Add back after adding encodings.
xdescribe "with width, pattern" do
it "returns a String in the compatible encoding" do
str = "abc".force_encoding Encoding::IBM437
result = str.center 6, "あ"
result.should == "あabcああ"
result.encoding.should equal(Encoding::UTF_8)
end

it "raises an Encoding::CompatibilityError if the encodings are incompatible" do
pat = "ア".encode Encoding::EUC_JP
-> do
"あれ".center 5, pat
end.should raise_error(Encoding::CompatibilityError)
end
end
end
54 changes: 54 additions & 0 deletions src/string_object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,60 @@ ArrayObject *StringObject::chars(Env *env) {
return ary;
}

String create_padding(String padding, size_t length) {
size_t quotient = ::floor(length / padding.size());
size_t remainder = length % padding.size();
auto buffer = new String { "" };

for (size_t i = 0; i < quotient; ++i) {
buffer->append(padding);
}

for (size_t j = 0; j < remainder; ++j) {
buffer->append_char(padding[j]);
}

return buffer;
}

Value StringObject::center(Env *env, Value length, Value padstr) {
nat_int_t length_i = length->to_int(env)->to_nat_int_t();

auto to_str = "to_str"_s;
String pad;

if (!padstr) {
pad = new String { " " };
} else if (padstr->is_string()) {
pad = padstr->as_string()->string();
} else if (padstr->respond_to(env, to_str)) {
auto result = padstr->send(env, to_str);

if (!result->is_string())
env->raise("TypeError", "padstr can't be converted to a string");

pad = result->as_string()->string();
} else {
env->raise("TypeError", "padstr can't be converted to a string");
}

if (pad.is_empty())
env->raise("ArgumentError", "padstr can't be empty");

if (length_i <= (nat_int_t)m_string.size())
return this;

double split = (length_i - m_string.size()) / 2.0;
auto left_split = ::floor(split);
auto right_split = ::ceil(split);

auto result = new String { m_string };
result->prepend(create_padding(pad, left_split));
result->append(create_padding(pad, right_split));

return new StringObject { result, m_encoding };
}

Value StringObject::chr(Env *env) {
if (this->is_empty()) {
return new StringObject { "", m_encoding };
Expand Down

0 comments on commit c87ab28

Please sign in to comment.