Brendon Laffey
Brendon Laffey

Reputation: 25

Ruby - Can someone explain each line of this code to me

I have to make a method that will add commas to numbers. I cannot use regular expressions in the method. After a lot of tinkering and research, I found this solution, that works:

def separate_comma(number)
    c = { :value => "", :length => 0 }
    r = number.to_s.reverse.split("").inject(c) do |t, e|
        iv, il = t[:value], t[:length]
        iv += ',' if il % 3 == 0 && il != 0
        { :value => iv + e, :length => il + 1 }
    end
    r[:value].reverse!
end

Can someone help me out here and explain the syntax for each line? There's a lot going on here, and I'm having a hard time grasping all of it. Also, any ideas on how to refactor it and make it more elegant-without using regex's. Thanks guys

Upvotes: 0

Views: 143

Answers (4)

oli5679
oli5679

Reputation: 1749

def separate_comma(number)
c = { :value => "", :length => 0 }

c is a hash with :value = "" and length = 0 (it will be used later)

r = number.to_s.reverse.split("").

the number is converted to a string, reversed and split into digits. Example:

19874 --> ["4","7","8","9","1"]

.inject(c) do |t, e|

Inject "Combines all elements of enum by applying a binary operation, specified by a block or a symbol that names a method or operator"

It transforms a 'memo' using the current array item, resulting in a new memo that is used for the next array item.

http://ruby-doc.org/core-2.2.1/Enumerable.html#method-i-inject

In this case each array item will modify the 'c' hash

    iv, il = t[:value], t[:length]

iv initially equals c:value and il equals c:length

    iv += ',' if il % 3 == 0 && il != 0

this will add a comma before the current array item if the current count is a multiple of three ( im my example this will only apply to the 4th array item (this will be clear later)

    { :value => iv + e, :length => il + 1 }

the (possibly modified) array item is appended to the string in our t[:value] hash and the :length is incremented by one

end

working through my example

1

iv = ""
il = 0
e = "4"
returns { :value "4", :length 1}

2

iv = "4"
il = 1
e = "7"
returns { :value "47", :length 2}

3

iv = "47"
il = 2
e = "8"
returns { :value "478":, :length 3}

4

iv = "478"
il = 3
e = "9," (the if statement returns true and so a comma is appended to the 9)
returns { :value "478,9", :length 4}

5

iv = "478,9"
il = 4
e = 1
returns { :value "478,91", :length 5} 

(this is returned by the inject statement)

r[:value].reverse!

takes the value returned by the inject statement and reverses it's characters. "19,874" end

In terms of making the code easier to understand, you could use these more descriptive variable names:

c = number_hash_memo
r = result
t = number_hash
e = char
iv = hash_val
il = hash_length

Upvotes: 2

marmeladze
marmeladze

Reputation: 6564

this would be easier

  1. take number (eg: number => 12345678)
  2. convert it to string ("12345678")
  3. reverse it ("87654321")
  4. convert it to array (["8", "7", "6", "5", "4", "3", "2", "1"])
  5. try to make subarrays with 3 element # the last one will contain remainder ( [["8", "7", "6"], ["5", "4", "3"], ["2", "1"]])
  6. iterate over subarray to push comma to the last place of subarrays, except last one
  7. join array to make a string
  8. reverse the string

here is the code i've written

def comma_seperate(number)
    s = number.to_s.reverse.split(//).each_slice(3).to_a
    i = 0
    while i<s.length-1
        s[i].push(",")
        i+=1
    end
    s.join().reverse
end

Upvotes: 0

Tucker
Tucker

Reputation: 659

c = { :value => "", :length => 0 }

c is a hash that contains two key-pairs, the first key is value an empty string, the second is length which is 0.

The purpose of this line was used to create the initial state of the object entered into the inject method.

number.to_s.reverse.split("")

Will return an array of 'number' in reversed order, for example:

number = 12345 number.to_s.reverse.split("") => ["5", "4", "3", "2", "1"]

r = number.to_s.reverse.split("").inject(c) do |t, e|

Enumerable#inject is a fancy method in the ruby community. It's taking in a hash and a block. The initial state of the object is the hash you input inside the inject method. The 't' in |t,e| is an object set by the return value of the last line of code in the inject block and its initial state is the hash entered into the inject method. And 'e' in |t,e| is the element in the array you are iterating over.

iv, il = t[:value], t[:length]

These are values you are setting throughout each iteration.

iv += ',' if il % 3 == 0 && il != 0

Under the condition that the length is 3 and not 0 add a comma. So after every three iteration a comma is added into your value object.

{ :value => iv + e, :length => il + 1 }

The return value of the last line in the Enumerable#inject method is passed to the 't' object.

r[:value].reverse!

The block of code will return 't'. Then finally the value is reversed back to its original state and returned.

Upvotes: 1

tompave
tompave

Reputation: 12412

What you posted works, but it's a very inefficient solution. Rather than refactoring it, it's better to just rewrite it:

def separate_comma(number)
  digits = number.to_s.reverse.split('')
  buffer = []
  buffer << digits.shift(3).join until digits.empty?
  buffer.join(',').reverse
end

This should also be much easier to follow.

Upvotes: 0

Related Questions