Skip to content

Commit b25695e

Browse files
authored
Add Int.from_digits as inverse of Int#digits (#16237)
1 parent 065dde3 commit b25695e

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

spec/std/int_spec.cr

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ private macro it_converts_to_s(num, str, **opts)
1212
end
1313
end
1414

15+
private class IntEnumerable
16+
include Enumerable(Int32)
17+
18+
def initialize(@elements : Array(Int32))
19+
end
20+
21+
def each(&)
22+
@elements.each do |e|
23+
yield e
24+
end
25+
end
26+
end
27+
1528
describe "Int" do
1629
describe "#integer?" do
1730
{% for int in BUILTIN_INTEGER_TYPES %}
@@ -1139,4 +1152,48 @@ describe "Int" do
11391152
end
11401153
end
11411154
end
1155+
1156+
describe "from_digits" do
1157+
it "returns Int composed from given digits" do
1158+
Int32.from_digits([9, 8, 7, 6, 5, 4, 3, 2, 1]).should eq(123456789)
1159+
end
1160+
1161+
it "works with a base" do
1162+
Int32.from_digits([11, 7], 16).should eq(123)
1163+
Int32.from_digits([11, 7], base: 16).should eq(123)
1164+
end
1165+
1166+
it "accepts digits as Enumerable" do
1167+
enumerable = IntEnumerable.new([11, 7])
1168+
Int32.from_digits(enumerable, 16).should eq(123)
1169+
end
1170+
1171+
it "raises for base less than 2" do
1172+
[-1, 0, 1].each do |base|
1173+
expect_raises(ArgumentError, "Invalid base #{base}") do
1174+
Int32.from_digits([1, 2, 3], base)
1175+
end
1176+
end
1177+
end
1178+
1179+
it "raises for digits greater than base" do
1180+
expect_raises(ArgumentError, "Invalid digit 2 for base 2") do
1181+
Int32.from_digits([1, 0, 2], 2)
1182+
end
1183+
1184+
expect_raises(ArgumentError, "Invalid digit 10 for base 2") do
1185+
Int32.from_digits([1, 0, 10], 2)
1186+
end
1187+
end
1188+
1189+
it "raises for negative digits" do
1190+
expect_raises(ArgumentError, "Invalid digit -1") do
1191+
Int32.from_digits([1, 2, -1])
1192+
end
1193+
end
1194+
1195+
it "works properly for values close to the upper limit" do
1196+
UInt8.from_digits([5, 5, 2]).should eq(255)
1197+
end
1198+
end
11421199
end

src/int.cr

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2776,3 +2776,53 @@ struct UInt128
27762776
self
27772777
end
27782778
end
2779+
2780+
# Returns a number for given digits and base.
2781+
# The digits are expected as an Enumerable with the least significant digit as the first element.
2782+
#
2783+
# Base must not be less than 2.
2784+
#
2785+
# All digits must be within 0...base.
2786+
#
2787+
# ```
2788+
# Int32.from_digits([5, 4, 3, 2, 1]) # => 12345
2789+
# Int32.from_digits([4, 6, 6, 0, 5], base: 7) # => 12345
2790+
# Int32.from_digits([45, 23, 1], base: 100) # => 12345
2791+
#
2792+
# Int32.from_digits([1], base: -2) # raises ArgumentError
2793+
# Int32.from_digits([-1]) # raises ArgumentError
2794+
# Int32.from_digits([3], base: 2) # raises ArgumentError
2795+
# ```
2796+
{% for type in %w(Int8 Int16 Int32 Int64 Int128 UInt8 UInt16 UInt32 UInt64 UInt128) %}
2797+
def {{type.id}}.from_digits(digits : Enumerable(Int), base : Int = 10) : self
2798+
if base < 2
2799+
raise ArgumentError.new("Invalid base #{base}")
2800+
end
2801+
2802+
num : {{type.id}} = 0
2803+
multiplier : {{type.id}} = 1
2804+
first_element = true
2805+
2806+
digits.each do |digit|
2807+
if digit < 0
2808+
raise ArgumentError.new("Invalid digit #{digit}")
2809+
end
2810+
2811+
if digit >= base
2812+
raise ArgumentError.new("Invalid digit #{digit} for base #{base}")
2813+
end
2814+
2815+
# don't calculate multiplier upfront for the next digit
2816+
# to avoid overflow at the last iteration
2817+
if first_element
2818+
first_element = false
2819+
else
2820+
multiplier *= base
2821+
end
2822+
2823+
num += digit * multiplier
2824+
end
2825+
2826+
num
2827+
end
2828+
{% end %}

0 commit comments

Comments
 (0)