diff --git a/spec/std/big/big_int_spec.cr b/spec/std/big/big_int_spec.cr index d653f75523ac..cb72cdc12576 100644 --- a/spec/std/big/big_int_spec.cr +++ b/spec/std/big/big_int_spec.cr @@ -1,6 +1,13 @@ require "spec" require "big" +private def it_converts_to_s(num, str, *, file = __FILE__, line = __LINE__, **opts) + it file: file, line: line do + num.to_s(**opts).should eq(str), file: file, line: line + String.build { |io| num.to_s(io, **opts) }.should eq(str), file: file, line: line + end +end + describe "BigInt" do it "creates with a value of zero" do BigInt.new.to_s.should eq("0") @@ -328,14 +335,86 @@ describe "BigInt" do result.to_s.should eq("10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376") end - it "does to_s in the given base" do - a = BigInt.new("1234567890123456789") - b = "1000100100010000100001111010001111101111010011000000100010101" - c = "112210f47de98115" - d = "128gguhuuj08l" - a.to_s(2).should eq(b) - a.to_s(16).should eq(c) - a.to_s(32).should eq(d) + describe "#to_s" do + context "base and upcase parameters" do + a = BigInt.new("1234567890123456789") + it_converts_to_s a, "1000100100010000100001111010001111101111010011000000100010101", base: 2 + it_converts_to_s a, "112210f47de98115", base: 16 + it_converts_to_s a, "112210F47DE98115", base: 16, upcase: true + it_converts_to_s a, "128gguhuuj08l", base: 32 + it_converts_to_s a, "128GGUHUUJ08L", base: 32, upcase: true + it_converts_to_s a, "1tckI1NfUnH", base: 62 + + # ensure case is same as for primitive integers + it_converts_to_s 10.to_big_i, 10.to_s(62), base: 62 + + it_converts_to_s (-a), "-1000100100010000100001111010001111101111010011000000100010101", base: 2 + it_converts_to_s (-a), "-112210f47de98115", base: 16 + it_converts_to_s (-a), "-112210F47DE98115", base: 16, upcase: true + it_converts_to_s (-a), "-128gguhuuj08l", base: 32 + it_converts_to_s (-a), "-128GGUHUUJ08L", base: 32, upcase: true + it_converts_to_s (-a), "-1tckI1NfUnH", base: 62 + + it_converts_to_s 16.to_big_i ** 1000, "1#{"0" * 1000}", base: 16 + + it "raises on base 1" do + expect_raises(ArgumentError, "Invalid base 1") { a.to_s(1) } + expect_raises(ArgumentError, "Invalid base 1") { a.to_s(IO::Memory.new, 1) } + end + + it "raises on base 37" do + expect_raises(ArgumentError, "Invalid base 37") { a.to_s(37) } + expect_raises(ArgumentError, "Invalid base 37") { a.to_s(IO::Memory.new, 37) } + end + + it "raises on base 62 with upcase" do + expect_raises(ArgumentError, "upcase must be false for base 62") { a.to_s(62, upcase: true) } + expect_raises(ArgumentError, "upcase must be false for base 62") { a.to_s(IO::Memory.new, 62, upcase: true) } + end + end + + context "precision parameter" do + it_converts_to_s 0.to_big_i, "", precision: 0 + it_converts_to_s 0.to_big_i, "0", precision: 1 + it_converts_to_s 0.to_big_i, "00", precision: 2 + it_converts_to_s 0.to_big_i, "00000", precision: 5 + it_converts_to_s 0.to_big_i, "0" * 200, precision: 200 + + it_converts_to_s 1.to_big_i, "1", precision: 0 + it_converts_to_s 1.to_big_i, "1", precision: 1 + it_converts_to_s 1.to_big_i, "01", precision: 2 + it_converts_to_s 1.to_big_i, "00001", precision: 5 + it_converts_to_s 1.to_big_i, "#{"0" * 199}1", precision: 200 + + it_converts_to_s 2.to_big_i, "2", precision: 0 + it_converts_to_s 2.to_big_i, "2", precision: 1 + it_converts_to_s 2.to_big_i, "02", precision: 2 + it_converts_to_s 2.to_big_i, "00002", precision: 5 + it_converts_to_s 2.to_big_i, "#{"0" * 199}2", precision: 200 + + it_converts_to_s -1.to_big_i, "-1", precision: 0 + it_converts_to_s -1.to_big_i, "-1", precision: 1 + it_converts_to_s -1.to_big_i, "-01", precision: 2 + it_converts_to_s -1.to_big_i, "-00001", precision: 5 + it_converts_to_s -1.to_big_i, "-#{"0" * 199}1", precision: 200 + + it_converts_to_s 123.to_big_i, "123", precision: 0 + it_converts_to_s 123.to_big_i, "123", precision: 1 + it_converts_to_s 123.to_big_i, "123", precision: 2 + it_converts_to_s 123.to_big_i, "00123", precision: 5 + it_converts_to_s 123.to_big_i, "#{"0" * 197}123", precision: 200 + + a = 2.to_big_i ** 1024 - 1 + it_converts_to_s a, "#{"1" * 1024}", base: 2, precision: 1023 + it_converts_to_s a, "#{"1" * 1024}", base: 2, precision: 1024 + it_converts_to_s a, "0#{"1" * 1024}", base: 2, precision: 1025 + it_converts_to_s a, "#{"0" * 976}#{"1" * 1024}", base: 2, precision: 2000 + + it_converts_to_s (-a), "-#{"1" * 1024}", base: 2, precision: 1023 + it_converts_to_s (-a), "-#{"1" * 1024}", base: 2, precision: 1024 + it_converts_to_s (-a), "-0#{"1" * 1024}", base: 2, precision: 1025 + it_converts_to_s (-a), "-#{"0" * 976}#{"1" * 1024}", base: 2, precision: 2000 + end end it "does to_big_f" do diff --git a/spec/std/int_spec.cr b/spec/std/int_spec.cr index a5d111ba5a6c..67ae40774793 100644 --- a/spec/std/int_spec.cr +++ b/spec/std/int_spec.cr @@ -4,12 +4,13 @@ require "./spec_helper" {% end %} require "spec/helpers/iterate" -private def to_s_with_io(num) - String.build { |io| num.to_s(io) } -end - -private def to_s_with_io(num, base, upcase = false) - String.build { |io| num.to_s(io, base, upcase: upcase) } +private macro it_converts_to_s(num, str, **opts) + it {{ "converts #{num} to #{str}" }} do + num = {{ num }} + str = {{ str }} + num.to_s({{ opts.double_splat }}).should eq(str) + String.build { |io| num.to_s(io, {{ opts.double_splat }}) }.should eq(str) + end end describe "Int" do @@ -166,79 +167,116 @@ describe "Int" do it "doesn't silently overflow" { 2_000_000.lcm(3_000_000).should eq(6_000_000) } end - describe "to_s in base" do - it { 12.to_s(2).should eq("1100") } - it { -12.to_s(2).should eq("-1100") } - it { -123456.to_s(2).should eq("-11110001001000000") } - it { 1234.to_s(16).should eq("4d2") } - it { -1234.to_s(16).should eq("-4d2") } - it { 1234.to_s(36).should eq("ya") } - it { -1234.to_s(36).should eq("-ya") } - it { 1234.to_s(16, upcase: true).should eq("4D2") } - it { -1234.to_s(16, upcase: true).should eq("-4D2") } - it { 1234.to_s(36, upcase: true).should eq("YA") } - it { -1234.to_s(36, upcase: true).should eq("-YA") } - it { 0.to_s(2).should eq("0") } - it { 0.to_s(16).should eq("0") } - it { 1.to_s(2).should eq("1") } - it { 1.to_s(16).should eq("1") } - it { 0.to_s(62).should eq("0") } - it { 1.to_s(62).should eq("1") } - it { 10.to_s(62).should eq("a") } - it { 35.to_s(62).should eq("z") } - it { 36.to_s(62).should eq("A") } - it { 61.to_s(62).should eq("Z") } - it { 62.to_s(62).should eq("10") } - it { 97.to_s(62).should eq("1z") } - it { 3843.to_s(62).should eq("ZZ") } - - it "raises on base 1" do - expect_raises(ArgumentError, "Invalid base 1") { 123.to_s(1) } - end - - it "raises on base 37" do - expect_raises(ArgumentError, "Invalid base 37") { 123.to_s(37) } - end - - it "raises on base 62 with upcase" do - expect_raises(ArgumentError, "upcase must be false for base 62") { 123.to_s(62, upcase: true) } - end - - it { to_s_with_io(12, 2).should eq("1100") } - it { to_s_with_io(-12, 2).should eq("-1100") } - it { to_s_with_io(-123456, 2).should eq("-11110001001000000") } - it { to_s_with_io(1234, 16).should eq("4d2") } - it { to_s_with_io(-1234, 16).should eq("-4d2") } - it { to_s_with_io(1234, 36).should eq("ya") } - it { to_s_with_io(-1234, 36).should eq("-ya") } - it { to_s_with_io(1234, 16, upcase: true).should eq("4D2") } - it { to_s_with_io(-1234, 16, upcase: true).should eq("-4D2") } - it { to_s_with_io(1234, 36, upcase: true).should eq("YA") } - it { to_s_with_io(-1234, 36, upcase: true).should eq("-YA") } - it { to_s_with_io(0, 2).should eq("0") } - it { to_s_with_io(0, 16).should eq("0") } - it { to_s_with_io(1, 2).should eq("1") } - it { to_s_with_io(1, 16).should eq("1") } - it { to_s_with_io(0, 62).should eq("0") } - it { to_s_with_io(1, 62).should eq("1") } - it { to_s_with_io(10, 62).should eq("a") } - it { to_s_with_io(35, 62).should eq("z") } - it { to_s_with_io(36, 62).should eq("A") } - it { to_s_with_io(61, 62).should eq("Z") } - it { to_s_with_io(62, 62).should eq("10") } - it { to_s_with_io(97, 62).should eq("1z") } - it { to_s_with_io(3843, 62).should eq("ZZ") } - - it "raises on base 1 with io" do - expect_raises(ArgumentError, "Invalid base 1") { to_s_with_io(123, 1) } - end - - it "raises on base 37 with io" do - expect_raises(ArgumentError, "Invalid base 37") { to_s_with_io(123, 37) } - end - - it "raises on base 62 with upcase with io" do - expect_raises(ArgumentError, "upcase must be false for base 62") { to_s_with_io(12, 62, upcase: true) } + describe "#to_s" do + it_converts_to_s 0, "0" + it_converts_to_s 1, "1" + + context "extrema for various int sizes" do + it_converts_to_s 127_i8, "127" + it_converts_to_s -128_i8, "-128" + + it_converts_to_s 32767_i16, "32767" + it_converts_to_s -32768_i16, "-32768" + + it_converts_to_s 2147483647, "2147483647" + it_converts_to_s -2147483648, "-2147483648" + + it_converts_to_s 9223372036854775807_i64, "9223372036854775807" + it_converts_to_s -9223372036854775808_i64, "-9223372036854775808" + + it_converts_to_s 255_u8, "255" + it_converts_to_s 65535_u16, "65535" + it_converts_to_s 4294967295_u32, "4294967295" + it_converts_to_s 18446744073709551615_u64, "18446744073709551615" + end + + context "base and upcase parameters" do + it_converts_to_s 12, "1100", base: 2 + it_converts_to_s -12, "-1100", base: 2 + it_converts_to_s -123456, "-11110001001000000", base: 2 + it_converts_to_s 1234, "4d2", base: 16 + it_converts_to_s -1234, "-4d2", base: 16 + it_converts_to_s 1234, "ya", base: 36 + it_converts_to_s -1234, "-ya", base: 36 + it_converts_to_s 1234, "4D2", base: 16, upcase: true + it_converts_to_s -1234, "-4D2", base: 16, upcase: true + it_converts_to_s 1234, "YA", base: 36, upcase: true + it_converts_to_s -1234, "-YA", base: 36, upcase: true + it_converts_to_s 0, "0", base: 2 + it_converts_to_s 0, "0", base: 16 + it_converts_to_s 1, "1", base: 2 + it_converts_to_s 1, "1", base: 16 + it_converts_to_s 0, "0", base: 62 + it_converts_to_s 1, "1", base: 62 + it_converts_to_s 10, "a", base: 62 + it_converts_to_s 35, "z", base: 62 + it_converts_to_s 36, "A", base: 62 + it_converts_to_s 61, "Z", base: 62 + it_converts_to_s 62, "10", base: 62 + it_converts_to_s 97, "1z", base: 62 + it_converts_to_s 3843, "ZZ", base: 62 + + it "raises on base 1" do + expect_raises(ArgumentError, "Invalid base 1") { 123.to_s(1) } + expect_raises(ArgumentError, "Invalid base 1") { 123.to_s(IO::Memory.new, 1) } + end + + it "raises on base 37" do + expect_raises(ArgumentError, "Invalid base 37") { 123.to_s(37) } + expect_raises(ArgumentError, "Invalid base 37") { 123.to_s(IO::Memory.new, 37) } + end + + it "raises on base 62 with upcase" do + expect_raises(ArgumentError, "upcase must be false for base 62") { 123.to_s(62, upcase: true) } + expect_raises(ArgumentError, "upcase must be false for base 62") { 123.to_s(IO::Memory.new, 62, upcase: true) } + end + end + + context "precision parameter" do + it_converts_to_s 0, "", precision: 0 + it_converts_to_s 0, "0", precision: 1 + it_converts_to_s 0, "00", precision: 2 + it_converts_to_s 0, "00000", precision: 5 + it_converts_to_s 0, "0" * 200, precision: 200 + + it_converts_to_s 1, "1", precision: 0 + it_converts_to_s 1, "1", precision: 1 + it_converts_to_s 1, "01", precision: 2 + it_converts_to_s 1, "00001", precision: 5 + it_converts_to_s 1, "#{"0" * 199}1", precision: 200 + + it_converts_to_s 2, "2", precision: 0 + it_converts_to_s 2, "2", precision: 1 + it_converts_to_s 2, "02", precision: 2 + it_converts_to_s 2, "00002", precision: 5 + it_converts_to_s 2, "#{"0" * 199}2", precision: 200 + + it_converts_to_s -1, "-1", precision: 0 + it_converts_to_s -1, "-1", precision: 1 + it_converts_to_s -1, "-01", precision: 2 + it_converts_to_s -1, "-00001", precision: 5 + it_converts_to_s -1, "-#{"0" * 199}1", precision: 200 + + it_converts_to_s 123, "123", precision: 0 + it_converts_to_s 123, "123", precision: 1 + it_converts_to_s 123, "123", precision: 2 + it_converts_to_s 123, "00123", precision: 5 + it_converts_to_s 123, "#{"0" * 197}123", precision: 200 + + it_converts_to_s 9223372036854775807_i64, "#{"1" * 63}", base: 2, precision: 62 + it_converts_to_s 9223372036854775807_i64, "#{"1" * 63}", base: 2, precision: 63 + it_converts_to_s 9223372036854775807_i64, "0#{"1" * 63}", base: 2, precision: 64 + it_converts_to_s 9223372036854775807_i64, "#{"0" * 137}#{"1" * 63}", base: 2, precision: 200 + + it_converts_to_s -9223372036854775808_i64, "-1#{"0" * 63}", base: 2, precision: 63 + it_converts_to_s -9223372036854775808_i64, "-1#{"0" * 63}", base: 2, precision: 64 + it_converts_to_s -9223372036854775808_i64, "-01#{"0" * 63}", base: 2, precision: 65 + it_converts_to_s -9223372036854775808_i64, "-#{"0" * 136}1#{"0" * 63}", base: 2, precision: 200 + + it "raises on negative precision" do + expect_raises(ArgumentError, "Precision must be non-negative") { 123.to_s(precision: -1) } + expect_raises(ArgumentError, "Precision must be non-negative") { 123.to_s(IO::Memory.new, precision: -1) } + end end end @@ -358,54 +396,6 @@ describe "Int" do end end - describe "to_s" do - it "does to_s for various int sizes" do - 0.to_s.should eq("0") - 1.to_s.should eq("1") - - 127_i8.to_s.should eq("127") - -128_i8.to_s.should eq("-128") - - 32767_i16.to_s.should eq("32767") - -32768_i16.to_s.should eq("-32768") - - 2147483647.to_s.should eq("2147483647") - -2147483648.to_s.should eq("-2147483648") - - 9223372036854775807_i64.to_s.should eq("9223372036854775807") - -9223372036854775808_i64.to_s.should eq("-9223372036854775808") - - 255_u8.to_s.should eq("255") - 65535_u16.to_s.should eq("65535") - 4294967295_u32.to_s.should eq("4294967295") - - 18446744073709551615_u64.to_s.should eq("18446744073709551615") - end - - it "does to_s for various int sizes with IO" do - to_s_with_io(0).should eq("0") - to_s_with_io(1).should eq("1") - - to_s_with_io(127_i8).should eq("127") - to_s_with_io(-128_i8).should eq("-128") - - to_s_with_io(32767_i16).should eq("32767") - to_s_with_io(-32768_i16).should eq("-32768") - - to_s_with_io(2147483647).should eq("2147483647") - to_s_with_io(-2147483648).should eq("-2147483648") - - to_s_with_io(9223372036854775807_i64).should eq("9223372036854775807") - to_s_with_io(-9223372036854775808_i64).should eq("-9223372036854775808") - - to_s_with_io(255_u8).should eq("255") - to_s_with_io(65535_u16).should eq("65535") - to_s_with_io(4294967295_u32).should eq("4294967295") - - to_s_with_io(18446744073709551615_u64).should eq("18446744073709551615") - end - end - describe "step" do it "steps through limit" do passed = false diff --git a/src/big/big_int.cr b/src/big/big_int.cr index c5625d6a1042..11d76df492e2 100644 --- a/src/big/big_int.cr +++ b/src/big/big_int.cr @@ -410,36 +410,94 @@ struct BigInt < Int # TODO: check hash equality for numbers >= 2**63 def_hash to_i64! - # Returns a string representation of self. - # - # ``` - # require "big" - # - # BigInt.new("123456789101101987654321").to_s # => 123456789101101987654321 - # ``` - def to_s : String - String.new(to_cstr) + def to_s(base : Int = 10, *, precision : Int = 1, upcase : Bool = false) : String + raise ArgumentError.new("Invalid base #{base}") unless 2 <= base <= 36 || base == 62 + raise ArgumentError.new("upcase must be false for base 62") if upcase && base == 62 + raise ArgumentError.new("Precision must be non-negative") unless precision >= 0 + + case {self, precision} + when {0, 0} + "" + when {0, 1} + "0" + when {1, 1} + "1" + else + count = LibGMP.sizeinbase(self, base).to_i + negative = self < 0 + + if precision <= count + len = count + (negative ? 1 : 0) + String.new(len + 1) do |buffer| # null terminator required by GMP + LibGMP.get_str(buffer, upcase ? -base : base, self) + base62_swapcase(Slice.new(buffer, len)) if base == 62 + {len, len} + end + else + len = precision + (negative ? 1 : 0) + String.new(len + 1) do |buffer| + # e.g. precision = 13, count = 8 + # "_____12345678\0" for positive + # "_____-12345678\0" for negative + LibGMP.get_str(buffer + precision - count, upcase ? -base : base, self) + base62_swapcase(Slice.new(buffer + len - count, count)) if base == 62 + + if negative + buffer.value = '-'.ord.to_u8 + buffer += 1 + end + Intrinsics.memset(buffer, '0'.ord.to_u8, precision - count, false) + + {len, len} + end + end + end end - # :ditto: - def to_s(io : IO) : Nil - str = to_cstr - io.write_utf8 Slice.new(str, LibC.strlen(str)) + def to_s(io : IO, base : Int = 10, *, precision : Int = 1, upcase : Bool = false) : Nil + raise ArgumentError.new("Invalid base #{base}") unless 2 <= base <= 36 || base == 62 + raise ArgumentError.new("upcase must be false for base 62") if upcase && base == 62 + raise ArgumentError.new("Precision must be non-negative") unless precision >= 0 + + case {self, precision} + when {0, 0} + # do nothing + when {0, 1} + io << '0' + when {1, 1} + io << '1' + else + count = LibGMP.sizeinbase(self, base).to_i + ptr = LibGMP.get_str(nil, upcase ? -base : base, self) + negative = self < 0 + + if precision <= count + buffer = Slice.new(ptr, count + (negative ? 1 : 0)) + else + if negative + io << '-' + ptr += 1 # this becomes the absolute value + end + + (precision - count).times { io << '0' } + buffer = Slice.new(ptr, count) + end + + base62_swapcase(buffer) if base == 62 + io.write_utf8 buffer + end end - # Returns a string containing the representation of big radix base (2 through 36). - # - # ``` - # require "big" - # - # BigInt.new("123456789101101987654321").to_s(8) # => "32111154373025463465765261" - # BigInt.new("123456789101101987654321").to_s(16) # => "1a249b1f61599cd7eab1" - # BigInt.new("123456789101101987654321").to_s(36) # => "k3qmt029k48nmpd" - # ``` - def to_s(base : Int) : String - raise ArgumentError.new("Invalid base #{base}") unless 2 <= base <= 36 - cstr = LibGMP.get_str(nil, base, self) - String.new(cstr) + private def base62_swapcase(buffer) + buffer.map! do |x| + # for ASCII integers as returned by GMP the only possible characters are + # '\0', '-', '0'..'9', 'A'..'Z', and 'a'..'z' + if x & 0x40 != 0 # 'A'..'Z', 'a'..'z' + x ^ 0x20 + else # '\0', '-', '0'..'9' + x + end + end end # :nodoc: @@ -606,10 +664,6 @@ struct BigInt < Int pointerof(@mpz) end - private def to_cstr - LibGMP.get_str(nil, 10, mpz) - end - def to_unsafe mpz end diff --git a/src/int.cr b/src/int.cr index 57114be4e52b..e0bcfa2fe674 100644 --- a/src/int.cr +++ b/src/int.cr @@ -616,41 +616,115 @@ struct Int private DIGITS_UPCASE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" private DIGITS_BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - def to_s(base : Int = 10, *, upcase : Bool = false) : String + # Returns a string representation of this integer. + # + # *base* specifies the radix of the returned string, and must be either 62 or + # a number between 2 and 36. By default, digits above 9 are represented by + # ASCII lowercase letters (`a` for 10, `b` for 11, etc.), but uppercase + # letters may be used if *upcase* is `true`, unless base 62 is used. In that + # case, lowercase letters are used for 10 to 35, and uppercase ones for 36 to + # 61, and *upcase* must be `false`. + # + # *precision* specifies the minimum number of digits in the returned string. + # If there are fewer digits than this number, the string is left-padded by + # zeros. If `self` and *precision* are both zero, returns an empty string. + # + # ``` + # 1234.to_s # => "1234" + # 1234.to_s(2) # => "10011010010" + # 1234.to_s(16) # => "4d2" + # 1234.to_s(16, upcase: true) # => "4D2" + # 1234.to_s(36) # => "ya" + # 1234.to_s(62) # => "jU" + # 1234.to_s(precision: 2) # => "1234" + # 1234.to_s(precision: 6) # => "001234" + # ``` + def to_s(base : Int = 10, *, precision : Int = 1, upcase : Bool = false) : String raise ArgumentError.new("Invalid base #{base}") unless 2 <= base <= 36 || base == 62 raise ArgumentError.new("upcase must be false for base 62") if upcase && base == 62 + raise ArgumentError.new("Precision must be non-negative") unless precision >= 0 - case self - when 0 + case {self, precision} + when {0, 0} + "" + when {0, 1} "0" - when 1 + when {1, 1} "1" else - internal_to_s(base, upcase) do |ptr, count| - String.new(ptr, count, count) + internal_to_s(base, precision, upcase) do |ptr, count, negative| + # reuse the `chars` buffer in `internal_to_s` if possible + if precision <= count || precision <= 128 + if precision > count + difference = precision - count + ptr -= difference + Intrinsics.memset(ptr, '0'.ord.to_u8, difference, false) + count += difference + end + + if negative + ptr -= 1 + ptr.value = '-'.ord.to_u8 + count += 1 + end + + String.new(ptr, count, count) + else + len = precision + (negative ? 1 : 0) + String.new(len) do |buffer| + if negative + buffer.value = '-'.ord.to_u8 + buffer += 1 + end + + Intrinsics.memset(buffer, '0'.ord.to_u8, precision - count, false) + ptr.copy_to(buffer + precision - count, count) + {len, len} + end + end end end end - def to_s(io : IO, base : Int = 10, *, upcase : Bool = false) : Nil + # Appends a string representation of this integer to the given *io*. + # + # *base* specifies the radix of the written string, and must be either 62 or + # a number between 2 and 36. By default, digits above 9 are represented by + # ASCII lowercase letters (`a` for 10, `b` for 11, etc.), but uppercase + # letters may be used if *upcase* is `true`, unless base 62 is used. In that + # case, lowercase letters are used for 10 to 35, and uppercase ones for 36 to + # 61, and *upcase* must be `false`. + # + # *precision* specifies the minimum number of digits in the written string. + # If there are fewer digits than this number, the string is left-padded by + # zeros. If `self` and *precision* are both zero, returns an empty string. + def to_s(io : IO, base : Int = 10, *, precision : Int = 1, upcase : Bool = false) : Nil raise ArgumentError.new("Invalid base #{base}") unless 2 <= base <= 36 || base == 62 raise ArgumentError.new("upcase must be false for base 62") if upcase && base == 62 + raise ArgumentError.new("Precision must be non-negative") unless precision >= 0 - case self - when 0 + case {self, precision} + when {0, 0} + # do nothing + when {0, 1} io << '0' - when 1 + when {1, 1} io << '1' else - internal_to_s(base, upcase) do |ptr, count| + internal_to_s(base, precision, upcase) do |ptr, count, negative| + io << '-' if negative + if precision > count + (precision - count).times { io << '0' } + end io.write_utf8 Slice.new(ptr, count) end end end - private def internal_to_s(base, upcase = false) + private def internal_to_s(base, precision, upcase = false) # Given sizeof(self) <= 128 bits, we need at most 128 bytes for a base 2 - # representation, plus one byte for the trailing 0. + # representation, plus one byte for the negative sign (possibly used by the + # string-returning overload). chars = uninitialized UInt8[129] ptr_end = chars.to_unsafe + 128 ptr = ptr_end @@ -666,13 +740,8 @@ struct Int num = num.tdiv(base) end - if neg - ptr -= 1 - ptr.value = '-'.ord.to_u8 - end - count = (ptr_end - ptr).to_i32 - yield ptr, count + yield ptr, count, neg end # Writes this integer to the given *io* in the given *format*.