Read data from yaml file and produce an array in ruby - ruby

I have the following data in a yaml file -
---
- :Subject_list
Subject 1:
:Act 1: A
:Act 2: B
Subject 2:
:Skill 1:
:Act 1: B
:Act 2: B
:Skill 2:
:Act 1: B
I need to read data from this file and and generate an output which is given below -
For subject 1 it will be like this as it has no skill level. Meaning the first element of the array is null.
["","Act 1", "A"], ["","Act 2", "B"]
For the second subject it will be like this -
["Skill 1","Act 1", "B"], ["","Act 2" "B"],["Skill 2","Act 1", "B"]
I am using these values to generate a prawn pdf table. Any help is greatly appreciated.
I tried doing this -
data=YAML::load(File.read("file.yaml"));
subject = data[:Subject_list]
sub_list =["Subject 1", "Subject 2"]
sub_list.each do |sub|
sub_data = []
sub_data = subject["#{sub}"]
# I convert the list symbol to an array, so i can loop through the sub activities.
#I need some direction here as how to check whether the symbol will be a skill or activity
end
Cheers!!

First off, your yaml file is not correct YAML, you cannot have keys like that, if you have space or weirdness in them you need to quote them, and what's up with the : at the beginning?
"Subject_list":
"Subject 1":
"Act 1": A
"Act 2": B
"Subject 2":
"Skill 1":
"Act 1": B
"Act 2": B
"Skill 2":
"Act 1": B
Then you need to load the file properly. You call the method load_file on the YAML module. No :: for method access in ruby afaik.
require 'yaml'
data = YAML.load_file "file.yaml"
subject = data["Subject_list"]
require 'pp'
subject.each do |s|
item = s.last
if item.keys.first =~ /Skill/
pp item.keys.inject([]) { |memo,x| item[x].map { |i| memo << i.flatten.unshift(x) } ; memo}
else
pp item.map { |k,v| ["", k, v] }
end
end

When building up a YAML file for data, especially a complex data structure, I let YAML generate it for me. Then I tweak as necessary:
require 'yaml'
require 'pp'
foo = ["Skill 1","Act 1", "B"], ["","Act 2" "B"],["Skill 2","Act 1", "B"]
puts foo.to_yaml
When I run that code I get this output:
---
- - Skill 1
- Act 1
- B
- - ""
- Act 2B
- - Skill 2
- Act 1
- B
You can prove the data is correctly generated by having YAML generate, then immediately parse the code and show what it looks like as the returned structure, letting you compare it, and by an equality check:
bar = YAML.load(foo.to_yaml)
pp bar
puts "foo == bar: #{ foo == bar }"
Which would output:
[["Skill 1", "Act 1", "B"], ["", "Act 2B"], ["Skill 2", "Act 1", "B"]]
foo == bar: true

Related

Takes a list of strings and returns each line prepended by the correct number: Ruby

Working on a task to create a function which takes a list of strings and returns each line prepended by the correct number.
## Prepend each number to a list of strings sequentially.
def number lines
lines.map {|numbered_lines| numbered_lines.prepend("1: ")}
end
## Problem: How to create a list of numbers that prepend to list of strings???
for example
number ["a", "b", "c"] # => ["1: a", "2: b", "3: c"]
My code currently outputs the following:
should add line numbers (1-based index)
Expected: ["1: a", "2: b", "3: c"], instead got: ["1: a", "1: b", "1: c"]
Any useful resources on how I can work around this type of task in future will be much appreciated. Thanks.
You can make use of with_index to start from index 1 instead of 0.
Iterates the given block for each element with an index, which starts from offset
Try the below:
def number_lines
["a", "b", "c"].map.with_index(1){ |element, index| "#{index}: #{element}" }
end
About with_index
By default with_index iterates the block for every element starting from 0th index
["a", "b", "c"].each.with_index { |element, index| puts "index: #{index} element: #{element}" }
The above snippet will output the below:
index: 0 element: a
index: 1 element: b
index: 2 element: c
with_index also accepts an optional argument to identify which index to start from, You can use with_index(3) to use the starting index as 3. Please find the below example for better understanding:
["a", "b", "c"].each.with_index(3) { |element, index| puts "index: #{index} element: #{element}" }
# Output
index: 3 element: a
index: 4 element: b
index: 5 element: c
Similarly to other answers, you could do
def number(lines)
(0...lines.size).each{|i| lines[i].prepend("#{i+1}: ")}
end
Note that this solution alters the input array itself and returns an enumerable range. If you don't want to modify the existing array, then I'd suggest:
def number(lines)
(0...lines.size).map{|i| "#{i+1}: #{lines[i]}" }
end
I often prefer iterating over the index range itself rather than the list if I need the index. It's a bit shorter and only has a single extra variable, but it's really just personal preference.
will it work for you?
def number lines
lines.each_with_index.map { |line,index| line.prepend("#{index} : ") }
end

.any? inside a case block not evaluating as expected

I have the following situation:
type = "stringX"
someArray = ["stringX", "string1", "string2"]
case type
when "stringA"
puts "a"
when "stringB"
puts "b"
when someArray.any? { |x| x.include?(type) }
puts "x"
when "stringC"
puts "c"
end
What I was expecting to happen was that it would go through the case and once it evaluates the .any? method as true (because by itself it does evaluate to true), it would puts "x". However, that's not what's happening here, it just goes through the rest of the case and reaches a raise somewhere below that.
I'm wondering what's going on here?
Use * operator
value = "stringX"
some_array = ["stringX", "string1", "string2"]
case type
when "stringA"
puts "a"
when "stringB"
puts "b"
when *some_array # notice the * before the variable name!
puts "x"
when "stringC"
puts "c"
end
How does this work?
when *some_array checks whether value is an element in some_array
For this particular case one should use the brilliant answer by #akuhn
Whether you need to put any random condition inside the case, you can do it using Proc#===:
type = "stringX"
someArray = ["stringX", "string1", "string2"]
case type
when "stringA" then puts "a"
when "stringB" then puts "b"
# ⇓⇓⇓⇓⇓⇓⇓⇓ HERE
when ->(type) { someArray.any? { |x| x.include?(type) } }
puts "x"
when "stringC" then puts "c"
end
EDIT: I will not delete the answer, because I think there might be something in it you didn't know before, but it does not work for your usecase. For that you should look at mudasobwas answer
It does not quite work this way, because basically the case statement will compare the given object with the object(s) passed to when, about similar to this:
if type == "stringA"
# ...
elsif type == "stringB"
# ...
and so on, unless you use an empty case statement.
case
when type == "stringA"
# ...
This is similar to an if elsif statement though, so you don't really see that very often.
In your case however, we can make use of Ruby's splat operator
case type
when "stringA"
puts "a"
when "stringB"
puts "b"
when *someArray
puts "x"
when "stringC"
puts "c"
Ruby's case statement can take multiple arguments with when which kind of works like an "or"
case "A"
when "B"
puts "B"
when "C", "A"
puts "C or A"
end
# => C or A
and the splat operator will fan out your array:
p ["a", "b"]
# => ["a", "b"]
p *["a", "b"]
# => "a"
# => "b"
p "a", "b"
# => "a"
# => "b"

How do I replace a for-loop in Ruby?

In Ruby it is bad style to use for-loops. This is commonly understood.
A style guide recommended to me:
(https://github.com/bbatsov/ruby-style-guide#source-code-layout)
says:
"Never use for, unless you know exactly why. Most of the time iterators should be used instead. for is implemented in terms of each (so you're adding a level of indirection), but with a twist - for doesn't introduce a new scope (unlike each) and variables defined in its block will be visible outside it."
The example given is:
arr = [1, 2, 3]
#bad
for elem in arr do
puts elem
end
# good
arr.each { |elem| puts elem }
I have researched and I can't find an explanation as to how to simulate a for loop that provides an iterating value I can pass to places or perform arithmetic on.
For example, with what would I replace:
for i in 0...size do
puts array1[i]
puts array2[size-1 - i]
puts i % 2
end
It's easy if it's one array, but I often need the current position for other purposes.
There's either a simple solution I'm missing, or situations where for is required. Additionally, I hear people talk about for as if it is never needed. What then is their solution to this?
Can it be improved? And what is the solution, if there is one? Thanks.
If you want to iterate over a collection and keep track of the index, use each_with_index:
fields = ["name", "age", "height"]
fields.each_with_index do |field,i|
puts "#{i}. #{field}" # 0. name, 1. age, 2. height
end
Your for i in 0...size example becomes:
array1.each_with_index do |item, i|
puts item
puts array2[size-1 - i]
puts i % 2
end
Don't forget you can do cool things like this too
fields = ["name", "age", "height"]
def output name, idx
puts "#{idx}. #{name}"
end
fields.each_with_index &method(:output)
Output
0. name
1. age
2. height
You can use this technique as a class or instance method too
class Printer
def self.output data
puts "raw: #{data}"
end
end
class Kanon < Printer
def initialize prefix
#prefix = prefix
end
def output data
puts "#{#prefix}: #{data}"
end
end
def print printer, data
# separating the block from `each` allows
# you to do interesting things
data.each &printer.method(:output)
end
example using class method
print Printer, ["a", "b", "c"]
# raw: a
# raw: b
# raw: c
example using instance method
kanon = Kanon.new "kanon prints pretty"
print kanon, ["a", "b", "c"]
# kanon prints pretty: a
# kanon prints pretty: b
# kanon prints pretty: c

ruby searching array for keywords

I am parsing a large CSV file in a ruby script and need to find the closest match for a title from some search keys. The search keys maybe one or more values and the values may not exactly match as per below (should be close)
search_keys = ["big", "bear"]
A large array containing data that I need to search through, only want to search on the title column:
array = [
["id", "title", "code", "description"],
["1", "once upon a time", "3241", "a classic story"],
["2", "a big bad wolf", "4235", "a little scary"],
["3", "three big bears", "2626", "a heart warmer"]
]
In this case I would want it to return the row ["3", "three big bears", "2626", "a heart warmer"] as this is the closest match to my search keys.
I want it to return the closest match from the search keys given.
Is there any helpers/libraries/gems I can use? Anyone done this before??
I am worried, this task should be handled to any search engine at db level or similar, no point fetching data in app and do searching across columns/rows etc, should be expensive. but for now here is the plain simple approach :)
array = [
["id", "title", "code", "description"],
["1", "once upon a time", "3241", "a classic story"],
["2", "a big bad wolf", "4235", "a little scary"],
["3", "three big bears", "2626", "a heart warmer"]
]
h = {}
search_keys = ["big", "bear"]
array[1..-1].each do |rec|
rec_id = rec[0].to_i
search_keys.each do |key|
if rec[1].include? key
h[rec_id] = h[rec_id] ? (h[rec_id]+1) : 1
end
end
end
closest = h.keys.first
h.each do |rec, count|
closest = rec if h[closest] < h[rec]
end
array[closest] # => desired output :)
I think you can do it by your self and no need to use any gems!
This may be close to what you need; searching in the array for the keys and set a rank for each found element.
result = []
array.each do |ar|
rank = 0
search_keys.each do |key|
if ar[1].include?(key)
rank += 1
end
end
if rank > 0
result << [rank, ar]
end
end
This code can be written better than the above, but i wanted to show you the details.
This works. Will find and return an array of matched* rows as result.
*matched rows = a row where the id, title, code or description match ANY of the provided seach_keys. incl partial searches such as 'bear' in 'bears'
result = []
array.each do |a|
a.each do |i|
search_keys.each do |k|
result << a if i.include?(k)
end
end
end
result.uniq!
You could probably write it in a more succinct way...
array = [
["id", "title", "code", "description"],
["1", "once upon a time", "3241", "a classic story"],
["2", "a big bad wolf", "4235", "a little scary"],
["3", "three big bears", "2626", "a heart warmer"]
]
search_keys = ["big", "bear"]
def sift(records, target_field, search_keys)
# find target_field index
target_field_index = nil
records.first.each_with_index do |e, i|
if e == target_field
target_field_index = i
break
end
end
if target_field_index.nil?
raise "Target field was not found"
end
# sums up which records have a match and how many keys they match
# key => val = record => number of keys matched
counter = Hash.new(0) # each new hash key is init'd with value of 0
records.each do |record| # look at all our given records
search_keys.each do |key| # check each search key on the field
if record[target_field_index].include?(key)
counter[record] += 1 # found a key, init to 0 if required and increment count
end
end
end
# find the result with the most search key matches
top_result = counter.to_a.reduce do |top, record|
if record[1] > top[1] # [0] = record, [1] = key hit count
top = record # set to new top
end
top # continue with reduce
end.first # only care about the record (not the key hit count)
end
puts "Top result: #{sift array, 'title', search_keys}"
# => Top result: ["3", "three big bears", "2626", "a heart warmer"]
Here is my one-line shot
p array.find_all {|a|a.join.scan(/#{search_keys.join("|")}/).length==search_keys.length}
=>[["3", "three big bears", "2626", "a heart warmer"]]
to get all the rows in order of number of matches
p array.drop(1).sort_by {|a|a.join.scan(/#{search_keys.join("|")}/).length}.reverse
Anyone knows how to combine the last solution so that the rows that contain none of the keys are dropped and to keep it concise as is ?

Ruby: OptionParser: String Arg & Hash Assignment

Using OptionParser for string argument input and hash assignment. What is the best way to read-in multiple variables for a single argument? How do I then assign those to a hash to reference? Here is what I have so far:
large_skus = Hash.new
small_skus = Hash.new
OptionParser.new do |opts|
opts.on("-b", "--brands bName1,bName2,bNameN", String, "Check specific brands by name") do |b|
options[:brands] = b.split(",")
end
opts.on("-l", "--large lSku1,lSku2,lSkuN", String, "Large SKUs - List CSVs") do |l|
options[:large_skus] = l.split(",")
##For each sku given
brandName = options[:brands]
large_skus[brandName] = l[$sku].to_i
##
end
opts.on("-s", "--small sSku1,sSku2,sSkuN", String, "Small SKUs - List CSVs") do |s|
options[:small_skus] = s.split(",")
##For each sku given
brandName = options[:brands]
small_skus[brandName] = s[$sku].to_i
##
end
end.parse!(ARGV)
Given an input of:
ruby test.rb --brands bName1,bName2,bNameN --large lSku1,lSku2,lSkuN --small wQueue1,wQueue2,wQueueN
This code
#!/usr/bin/env ruby
require 'ap'
require 'optparse'
options = {}
OptionParser.new do |opts|
opts.on("-b", "--brands bName1,bName2,bNameN", Array, "Check specific brands by name") do |b|
options[:brands] = b
end
opts.on("-l", "--large lSku1,lSku2,lSkuN", Array, "Large SKUs - List CSVs") do |l|
options[:large_skus] = l
end
opts.on("-s", "--small wQueue1,wQueue2,wQueueN", Array, "Small SKUs - List CSVs") do |s|
options[:small_skus] = s
end
end.parse!(ARGV)
ap options
Produces this output:
{
:brands => [
[0] "bName1",
[1] "bName2",
[2] "bNameN"
],
:large_skus => [
[0] "lSku1",
[1] "lSku2",
[2] "lSkuN"
],
:small_skus => [
[0] "wQueue1",
[1] "wQueue2",
[2] "wQueueN"
]
}
Notice that instead of using types of String for each option, I'm using Array. That lets OptionParser do the heavy lifting of parsing the elements into an array. From that point it's up to you what you do with the array elements.
I think you are approaching this the wrong way. You want your users to have to keep track of the order of the parameters they input but you don't want to do it yourself in the code!
How about you don't ask anybody to keep track of what goes with what and make it explicit:
ruby test.rb --input bName1,lSku1,wQueue1 --input bName2,lSku2,wQueue2 --input bNameN,lSkuN,wQueueN
Code:
opts.on("--input <brand,Large_Skus,Small_Skus>", "input description",
"NOTE: Can be used more than once.") do |opt|
list = opt.split(',')
unless list.lenght == 3
raise "some error because you didn't place all arguments"
end
options[:input].push list
end
result:
[ [ 'bName1', 'lSku1', 'wQueue1' ],
[ 'bName2', 'lSku2', 'wQueue2' ],
[ 'bNameN', 'lSkuN', 'wQueueN' ] ]

Resources