1

I have an array:

["Melanie", "149", "Joe", "2", "16", "216", "Sarah"]

I want to create a hash:

{"Melanie"=>[149], "Joe"=>[2, 16, 216] "Sarah"=>nil}

How would I accomplish this when the keys and values are in the same array?

All values would be integers (although they are in string form in the array.) All keys start and end with a letter.

Melanie Palen
  • 2,645
  • 6
  • 31
  • 50
  • 1
    Your expected "hash" is invalid. – sawa Jul 18 '15 at 17:25
  • @JKillan Don't change the question. You are not the OP. You do not know the OP's intention. – sawa Jul 18 '15 at 17:31
  • 1
    @sawa It was easy to make an accurate enough guess at what Melanie intended. There's no harm in correcting minor syntactical errors. – JKillian Jul 18 '15 at 18:35
  • @JKillian The OP's edit shows that your guess was wrong. Of course it is easy to make a wrong guess. – sawa Jul 18 '15 at 19:08
  • @sawa Not necessarily. The OP may have decided to edit the answer that way based on the (good) suggestions to use a consistent type for the values. Either way though, better to have some 'accurate enough' target to aim for than no target at all. – JKillian Jul 18 '15 at 19:26
  • 1
    @JKillian, why edit? If you're not 100% certain of the OP's intention, why not leave a comment instead? – Cary Swoveland Jul 18 '15 at 20:01
  • I'm voting to close this question as off-topic because awkward question plus awkward editing wars resulted in answer which doesn't (quite) match the question, all for a one-off code debug. – DreadPirateShawn Jul 19 '15 at 05:49

5 Answers5

6

Your expected hash is invalid. Therefore, it is impossible to get what you wrote that you want.

From your issue, it looks reasonable to expect the values to be array. In that case, you can do it like this:

["Melanie", "149", "Joe", "2", "16", "216", "Sarah"]
.slice_before(/[a-z]/i).map{|k, *v| [k, v.map(&:to_i)]}.to_h
# => {"Melanie"=>[149], "Joe"=>[2, 16, 216], "Sarah"=>[]}

With little modification, you can let the value be a number instead of an array when the array length is one, but that is not a good design; it would introduce exceptions.

sawa
  • 165,429
  • 45
  • 277
  • 381
  • 1
    This will work perfectly for what I need. Thank you so much! – Melanie Palen Jul 18 '15 at 17:43
  • Just be careful because if you have non-integer numbers or if you are using an older version of Ruby this won't work. Other than that though, this is a concise, fairly readable solution. – JKillian Jul 18 '15 at 18:41
2

Try this

def numeric?(x) 
 x.chars.all? { |y| ('0'..'9').include?(y) }
end

array = ["Melanie", "149", "Joe", "2", "16", "216", "Sarah"]
keys = array.select { |x| not numeric?(x) }

map = {}
keys.each do |k|
    from = array.index(k) + 1
    to = array.index( keys[keys.index(k) + 1] )
    map[k] = to ? array[from...to] : array[from..from]
end

p map

Output:

{"Melanie"=>["149"], "Joe"=>["2", "16", "216"], "Sarah"=>[]}
[Finished in 0.1s]
Wand Maker
  • 18,476
  • 8
  • 53
  • 87
2

Here's another way:

arr = ["Melanie", "149", "Joe", "2", "16", "216", "Sarah"]

class String
  def integer?
    !!(self =~ /^-?\d+$/)
  end
end

Hash[*arr.each_with_object([]) { |s,a| s.integer? ? a[-1] << s.to_i : a<<s<<[] }].
  tap { |h| h.each_key { |k| h[k] = nil if h[k].empty? } }
  #=> {"Melanie"=>[149], "Joe"=>[2, 16, 216], "Sarah"=>nil}  
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • Just curious - What does !! do in integer? method ? – Wand Maker Jul 18 '15 at 19:02
  • Got error - test.rb:11:in `
    ': undefined method `to_h' for [["Melanie", [149]], ["Joe", [2, 16, 216]], ["Sarah", []]]:Array (NoMethodError)
    – Wand Maker Jul 18 '15 at 19:06
  • 1
    `!!` is a trick for converting Ruby objects to `true` or `false`. As you know, in logical comparisons, `nil` and `false` ("falsy" values) evaluate to `false`, and all other Ruby objects ("truthy" values) evaluate as `true`. If `a` is truthy, `!a` #=> false`, so `!!a #=> true`. if `a` is falsy, `!a` #=> true`, so `!!a #=> false`. – Cary Swoveland Jul 18 '15 at 19:24
  • 1
    I should have mentioned that [Array#to_h](http://ruby-doc.org/core-2.2.0/Array.html#method-i-to_h) was introduced in Ruby v2.0. You are probably using an earlier version. If so, use [Hash::Hash](http://ruby-doc.org/core-2.2.0/Hash.html#method-c-5B-5D) (replace `a.to_h` with `Hash[a]`). – Cary Swoveland Jul 18 '15 at 19:29
  • I just noticed something interesting, and edited my answer as a result. I had always viewed `Array.to_h` as being a direct replacement of the class method `Hash::Hash`. It is not! The latter is more versatile. `Array#to_h` requires the receiver to be an array of two-element arrays, whereas `Hash::Hash` has three forms, the first of which (see the link in my previous comment) takes an argument with an even number of elements that are paired to form the hash's key-value pairs. [Note the splat (`*`) in my edit.] `Array#to_h` obviously has no counterpart to that. – Cary Swoveland Jul 18 '15 at 19:53
1

There are three components to your question, and I will try to answer them separately.

Regarding storing a multi-valued mapping, while there are specialized solutions available, the most common recommendation is just to store a hash whose values are arrays. That is, for your use case, your primary data structure is a hash whose keys are strings and whose values are arrays of integers. Depending on your desired behavior for duplicates etc., etc, you may wish to substitute a different data structure for the value structure, possibly a set.

Regarding identifying strings containing numbers and strings not containing numbers, well, that depends on exactly what your non-number-containing strings could instead contain, but a good starting point would be to perform a regular expression match for digits. You didn't specify whether your allowable numeric strings represented integers, floating points, etc. The particular answer to that may affect your overall strategy. Unfortunately, input parsing and validation is a complex and messy topic in the general case.

Regarding the actual conversion process, I would recommend the following strategy. Iterate through your input array. Check each string for whether it is numeric or non-numeric. If it is non-numeric, store that as the current key in a local. Also, in your hash, create a mapping from that key to a new empty array. If, instead, the string is numeric, convert it into a number, and add it to the array under the appropriate key.

Ming
  • 1,613
  • 12
  • 27
0

I don't know if there's a pretty way to do it. I'd do something like this:

def numeric?(string)
  # `!!` converts parsed number to `true`
  !!Kernel.Float(string) 
rescue TypeError, ArgumentError
  false
end

def my_method(input_array)
  # associate values with proper key and stores result in output
  curr_key = nil
  output = {}
  input_array.each do |e|
    if !numeric?(e)
      output[e] = []
      curr_key = e
    else 
      # use Float if values may be floating-point
      output[curr_key] << Integer(e, 10)
    end
  end

  output.each do |k, v|
      output[k] = v.empty? ? nil : v
  end

  output
end

Source for numeric method.

Community
  • 1
  • 1
JKillian
  • 18,061
  • 8
  • 41
  • 74