0
votes

I am trying to use a YAML file, reading from it and writing to it a list of values. On the first run of this script, the yaml file is correctly created, but then on the second it throws a conversion TypeError which I don't know to fix.

db_yml = 'store.yml'

require 'psych'
begin
    if File.exist?(db_yml)
        yml = Psych.load_file(db_yml)
        puts "done load"        

        yml['reminders']['reminder_a'] = [123,456] 
        yml['reminders']['reminder_b'] = [457,635,123]
        File.write(db_yml, Psych.dump(yml) ) 
    else 
      #the file does not exist yet, create an empty one.
        File.write(db_yml, Psych.dump(
        {'reminders' => [
            {'reminder_a'=> [nil]}, 
            {'reminder_b'=> [nil]}
        ]}
  )) #Store
    end
rescue IOError => msg  
  # display the system generated error message  
  puts msg 
end

produces the file store.yml on first run:

---
reminders:
- reminder_a:
  - 
- reminder_b:
  - 

So far so good. But then on the second run it fails with

done load
yamlstore.rb:23:in `[]=': no implicit conversion of String into Integer (TypeError)
        from yamlstore.rb:23:in `<main>'

Could you tell me where I am going wrong?

2

2 Answers

6
votes

The error message says that you were passing a String where Ruby expects something that is implicitly convertible to an Integer. The number one place where Ruby expects something that is implicitly convertible to an Integer is when indexing into an Array. So, whenever you see this error message, you can be 99% sure that you are either indexing an Array with something you thought was an Integer but isn't, or that you are indexing an Array that you thought was something else (most likely a Hash). (The other possibility is that you are trying to do arithmetic with a mix of Integers and Strings.)

Just because Ruby is a dynamically-typed programming language does not mean that you don't need to care about types. In particular, YAML is a (somewhat) typed serialization format.

The type of the file you are creating looks something like this:

Map<String, Sequence<Map<String, Sequence<Int | null>>>>

However, you are accessing it, as if it were typed like this:

Map<String, Map<String, Sequence<Int | null>>>

To put it more concretely, you are creating the value corresponding to the key 'reminders' as a sequence (in YAML terms, an Array in Ruby terms) of maps (Hashes). Arrays are indexed by Integers.

You, however, are indexing it with a String, as if it were a Hash.

So, you either need to change how you access the values like this:

yml['reminders'][0]['reminder_a'] = [123, 456]
#               ↑↑↑

yml['reminders'][1]['reminder_b'] = [457,635,123]
#               ↑↑↑

Or change the way you initialize the file like this:

File.write(db_yml, Psych.dump(
{ 'reminders' => {
#                ↑
    'reminder_a' => [nil],
#  ↑                     ↑
    'reminder_b' => [nil]
#  ↑                     ↑
}

so that the resulting YAML document looks like this:

---
reminders:
  reminder_a:
    - 
  reminder_b:
    - 
4
votes

There is nothing wrong with the YAML file. However you create the file you create it with the following structure:

yaml = {
  'reminders' => [
    {'reminder_a'=> [nil]}, 
    {'reminder_b'=> [nil]}
  ]
}

Notice that the contents of yaml['reminders'] is an array. Where it goes wrong is here:

reminders = yaml['reminders']
reminder_a = reminders['reminder_a'] # <= error

# in the question example:
# yml['reminders']['reminder_a'] = [123,456]

Since reminders is an array you can't access it by passing a string as index. You have 2 options:

  1. In my opinion the best option (if you want to access the reminders by key) is changing the structure to use a hash instead of an array:

    yaml = {
      'reminders' => {
        'reminder_a'=> [nil], 
        'reminder_b'=> [nil]
      }
    }
    

    With the above structure you can access your reminder through:

    yaml['reminders']['reminder_a']
    
  2. Somewhat clumsy, find the array element with the correct key:

    yaml['reminders'].each do |reminder|
      reminder['reminder_a'] = [123,456] if reminder.key? 'reminder_a'
      reminder['reminder_b'] = [457,635,123] if reminder.key? 'reminder_b'
    end