Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions spec/std/int_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ private macro it_converts_to_s(num, str, **opts)
end
end

private class IntEnumerable
include Enumerable(Int32)

def initialize(@elements : Array(Int32))
end

def each(&)
@elements.each do |e|
yield e
end
end
end

describe "Int" do
describe "#integer?" do
{% for int in BUILTIN_INTEGER_TYPES %}
Expand Down Expand Up @@ -1139,4 +1152,44 @@ describe "Int" do
end
end
end

describe "undigits" do
it "returns Int composed from given digits" do
Int.undigits([9, 8, 7, 6, 5, 4, 3, 2, 1]).should eq(123456789)
end

it "works with a base" do
Int.undigits([11, 7], 16).should eq(123)
Int.undigits([11, 7], base: 16).should eq(123)
end

it "accepts digits as Enumerable" do
enumerable = IntEnumerable.new([11, 7])
Int.undigits(enumerable, 16).should eq(123)
end

it "raises for base less than 2" do
[-1, 0, 1].each do |base|
expect_raises(ArgumentError, "Invalid base #{base}") do
Int.undigits([1, 2, 3], base)
end
end
end

it "raises for digits greater than base" do
expect_raises(ArgumentError, "Invalid digit 2 for base 2") do
Int.undigits([1, 0, 2], 2)
end

expect_raises(ArgumentError, "Invalid digit 10 for base 2") do
Int.undigits([1, 0, 10], 2)
end
end

it "raises for negative digits" do
expect_raises(ArgumentError, "Invalid digit -1") do
Int.undigits([1, 2, -1])
end
end
end
end
24 changes: 24 additions & 0 deletions src/int.cr
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,30 @@ struct Int
ary
end

def self.undigits(digits : Enumerable(Int), base : Int = 10) : self
if base < 2
raise ArgumentError.new("Invalid base #{base}")
end

num = 0
multiplier = 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these should not be hardcoded to Int32

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think this method should be defined on the concrete types (Int32, UInt8 etc.). Then self defines the return type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it OK to use macros in this case?

{% for type in %w(Int8 Int16 Int32 Int64 Int128 UInt8 UInt16 UInt32 UInt64 UInt128) %}
  def {{type.id}}.from_digits(digits : Enumerable(Int), base : Int = 10) : self
    if base < 2
      raise ArgumentError.new("Invalid base #{base}")
    end

    num = 0
    multiplier = 1

    # ...

    num
  end
{% end %}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be okay to define it on Int. It will only work on its concrete subclasses though.
Int.from_io works that way as well. Calling it directly doesn't compile, but Int32.from_io etc. work fine.

Obviously, it's confusing to have the method defined on Int when it doesn't work there. So maybe the macro solution is better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use a macro inherited hook instead of declaring on each child type directly. But for some reason, that doesn't seem to work on Int. Maybe because the inheritance is defined in the compiler? 🤔

struct Int
  macro inherited
    def self.from_digits(digits : Enumerable(Int), base : Int = 10) : self
      # ...
      ZERO    
    end
  end
end

Int32.from_digits([1, 2, 3, 4]) # Error: undefined method 'from_digits' for Int32.class

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inheritance is done in the compiler, so no hooks are called. See #13836


digits.each do |digit|
if digit < 0
raise ArgumentError.new("Invalid digit #{digit}")
end

if digit >= base
raise ArgumentError.new("Invalid digit #{digit} for base #{base}")
end

num += digit * multiplier
multiplier *= base
end

num
end

private DIGITS_DOWNCASE = "0123456789abcdefghijklmnopqrstuvwxyz"
private DIGITS_UPCASE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
private DIGITS_BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
Expand Down
Loading