2

I am trying to replace a component in a legacy system with a Ruby script. One piece of this system accepts a string that contains ASCII '0's and '1's apparently to represent a bitfield of locations. It then converts these location to a string of comma separated 2 two codes (mostly US states).

I have a Ruby method that does this but it doesn't seem like I am doing it the best way Ruby could. Ruby has a ton of ways built in to iterate over and manipulated array and I feel I am not using them to their fullest:

# input "0100010010" should return "AZ,PR,WY"
def locations(bits)          
  # Shortened from hundreds for this post. :u? is for locations I have't figured out yet.
  fields = [ :u?, :az, :de, :mi, :ne,    :wy, :u?, :u?, :pr, :u? ] 
  matches = []
  counter = 0
  fields.each { |f|
    case bits[counter]
      when '1' then matches << f
      when '0' then nil
      else raise "Unknown value in location bit field"
    end
    counter += 1
  }
  if matches.include(:u?) then raise "Unknown field bit set"  end
  matches.sort.join(",").upcase
end

What would be a better way to do this?

It seems counter to the "Ruby way" to have counter variables floating around. I tried looking at ways to use Array#map, and I could find nothing obvious. I also tried Googling for Ruby Idioms pertaining to Arrays.

3 Answers 3

5
matches = fields.select.with_index { |_,i| bits[i] == '1' }
# => [:az, :wy, :pr]

To verify bits only holds 0s and 1s, you can still do

raise "Unknown value in location bit field" if !bits.match(/^[01]*$/)
Sign up to request clarification or add additional context in comments.

Comments

2

Use Array#zip and Array#reduce

bits.split('').zip(fields).reduce([]) do |a, (k, v)| 
  k == '1' ? a << v.to_s.upcase : a
end.sort.join(',')
# => "AZ,PR,WY

Explanation:

1) split bits into an array of chars:

bits.split('')  # => ["0", "1", "0", "0", "0", "1", "0", "0", "1", "0"]

2) zip both arrays to generate an array of pairs (by position)

bits.split('').zip(fields) # => [["0", :u?], ["1", :az], ["0", :de], ["0", :mi], 
# ["0", :ne], ["1", :wy], ["0", :u?], ["0", :u?], ["1", :pr], ["0", :u?]] 

3) reduce the array taking the desired elements according to the conditions

.reduce([]) do |a, (k, v)| 
  k == '1' ? a << v.to_s.upcase : a
end  # => "[AZ,WY,PR]

4) sort the resulting array and join their elements to get the expected string

.sort.join(',') # => "AZ,PR,WY"

4 Comments

It would help if you could add a little explanation to this example.
@SteveFenton: I have added a little explanation, thanks.
@SteveFenton: nice post by the way. I really agree that we should face that way an interview: stevefenton.co.uk/Content/Blog/Date/201403/Blog/…
I had wanted to do something like this and I thought that connecting two related arrays would be a fairly common operation, but the name "zip" eluded me.
1

You could combine each_with_index, map andcompact:

fields.each_with_index.map do |v,i| 
    v if bits[i] == '1' 
end.compact

each_with_index returns an iterator for each each value and its integer index.

map uses the return value of a passed block to yield an output value for each input value. The block returns the value if its corresponding bit is set, and implicitly returns nil if it is not.

compact returns a copy of the output array with all of the nil values removed.

For more detail see the docs for Enumerable and Array.

2 Comments

Can also do map.with_index.
After rereading the page ruby-doc.org/core-2.1.1/Array.html#method-i-map I saw each_with_index and I thought this would be the best answer

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.