Ruby: transform a data structure into an excel sheet - ruby-on-rails

I have processed a log file and created a data structure of below format
values = [ {:VM=>"VM_US_OLA_1"}
{:id=>"OLFTB51", :datum=>{"LAP"=>"6.93817", "YCC_OWER"=>"1.0391"}}
{:id=>"OLFTB10", :datum=>{"LAP_2"=>"2.72646", "CFG_ON"=>"15.9489746", "YCC_ON"=>".401794"}}
{:VM=>"VM_ASIA_FLO_1"}
{:id=>"LOPMLAP", :datum=>{"LAP"=>"1.81048584", "FM_ON"=>".00"} ]
values is an array.
I'm trying to create a spreadsheet of below format where only the VM column gets highlighted in green and for every VM set, a blank line highlighted in 'yellow' should be inserted. I tried multiple approaches and went thru the axlsx documentation too but unable to get the desired format.
My excel snippet below:
require 'axlsx'
p = Axlsx::Package.new
p.workbook.add_worksheet(:name => "Statistics") do |sheet|
style1 = sheet.styles.add_style(:bg_color => "EF0920", :fg_color => "FFFFFF", b:true)
style2 = sheet.styles.add_style(:bg_color => "00FF00", :fg_color => "FFFFFF", b:true)
sheet.add_row ["VM", "NAME", "DETAILS", "OCC"], :style => style1
values.each do |val|
sheet.add_row [ val[:VM], val[:id] ], :style =>[style2, nil]
val[:datum].each do |k, v|
sheet << ["", "", k, v]
end
end
sheet.add_row
end
p.serialize 'Stat.xlsx'
Any suggestions should be really helpful here, many thanks.
Expected Output
Current Output
Adding Log file and my code
Log
---- vm name ----
VM_US_OLA_1
OLFTB51
OWNER IN_GB
------------------------------ ----------
LAP 6.93817
YCC_OWER 1.0391
=========================================
---- vm name ----
OLFTB10
OWNER IN_GB
------------------------------ ----------
LAP_2 2.7266846
CFG_ON 15.9489746
YCC_ON .401794
=========================================
---- vm name ----
VM_ASIA_FLO_1
LOPMLAP
OWNER IN_GB
------------------------------ ----------
LAP 1.81048584
FM_ON .00
=========================================
---- vm name ----
INGTY_2
OWNER IN_GB
------------------------------ ----------
=========================================
so on of the same format
Code to process the logs
require 'csv'
values = []
total = File.read("final.log")
total.each_line do |line|
line.strip!
next if line.empty?
next if line.include?('selected') || line.include?('IN_GB')
next if ['-','='].include? line[0]
parts = line.split ' '
if parts.size == 1 and line.start_with?('size')
values[current += 1] = {vm: line.strip}
next
elsif parts.size == 1 and parts = /^(?!.*size_).*$/
values[current += 1] = {id: line, datum: {}}
next
end
parts.each_cons(2) do |key, value|
values[current][:datum][key] = value
end
end
puts values

The problem is with the structure of your data. Try to change to have the values of your array in this structure
{:VM=>"VM_US_OLA_1", :id=>"OLFTB51", :datum=>{"LAP"=>"6.93817", "YCC_OWER"=>"1.0391"}
What's happening is VM is one record in your array, and the other data is another record in the array, you need to merge them so you access them in the same loop iteration
EDIT
I guess how we parse the log file is a bit tricky, so I am going to leave it but I am sure that this part I am going to introduce can be done in the log file parsing
First, we can change the structure of the array like that (before p.workbook.add_worksheet)
dataset = []
values.each_with_index do |value|
if value[:VM].present? # or :vm, not sure about the key
dataset << value
dataset.last[:data] = []
else
dataset.last[:data] << value
end
end
Then this loop: values.each do |val|
Can be changed to dataset.each do |val|
Then you can continue with the rest of your logic:
sheet.add_row [ val[:VM], val[:data][0][:id] ], :style =>[style2, nil]
val[data].each do |record|
record[:datanum].each do |k, v|
The problem with your previous code is, you used to add a new row sheet.add_row at the end of each iteration, so this was messing things up as you were expecting the data you need to be just right after your VM but a new line has already been inserted

Related

Ruby Seed - How to store subcategories for each sneaker and remove duplicates?

I want to create my database from CSV data. For that I have parse my file and I start to seed.
In my .csv file, there are sneakers name with subcategories like in the picture :
In my db/seeds.rb, i coded this :
# db/seeds.rb
csv_text = File.read(Rails.root.join('lib', 'seeds', 'air_jordan.csv'))
csv = CSV.parse(csv_text, :headers => true, :encoding => 'ISO-8859-1')
air_jordan_subcategories = ['1', '11', "Jordan OFF-WHITE", "Future", "Women", "High"]
csv.each do |row|
s = Sneaker.new
s.name = row['Name']
air_jordan_subcategories.each do |sub|
if row['Subcategory'] == sub
s.subcategory << sub
end
end
s.save
end
What I want is that the pair “Jordan 1 Mid Banned (2020)“, appears like this in my database:
=> #<Sneaker:0x00007ffdbd4a5980
id: 1,
name: "Jordan 1 Mid Banned (2020)",
subcategory : ["Mid", "1"]>
While for the moment, in my database I have 2 times the same pair because I have 2 different subcategories :
=> #<Sneaker:0x00007ffdbd4a5980
id : 1,
name : "Jordan 1 Mid Banned (2020)",
subcategory : ["Mid"]>
=> #<Sneaker:0x00007ffdbd4a5933
id: 2,
name : "Jordan 1 Mid Banned (2020)",
subcategory : ["1"] >
I can’t find a logical solution for this, what I would like is simply :
air_jordan_subcategories.each do |sub|
while row['category'] == sub
s.subcategory << sub
row += 1
end
end
But row += 1 doesn’t work, and while row['category'] == sub trigger an infinite loop.
Thanks for helping !

What causes CSV export cut off text in Ruby 2.0.0/Postgres 9.3/Rails 4.2

We just found out that CSV export cut off text in one field. Here is the original text for CSV export (from a text field of postgres 9.3):
#payable_approved_unpaid, #payable_paid, #payable_po_unpaid = {}, {}, {}
models.each do |m|
#payable_po_unpaid[m.id.to_s] = PurchaseOrderx::Order.where(project_id: m.id).sum('po_total') - PaymentRequestx::PaymentRequest.where(project_id: m.id).where(resource_string: 'purchase_orderx/orders').sum('amount')
#payable_paid[m.id.to_s] = PaymentRequestx::PaymentRequest.where(project_id: m.id).where(wf_state: :paid).sum('amount')
#payable_approved_unpaid[m.id.to_s] = PaymentRequestx::PaymentRequest.where(project_id: m.id).where('approved = ? AND wf_state != ?', true, :paid).sum('amount')
end
Here is what we get from CSV:
#payable_approved_unpaid, #payable_paid, #payable_po_unpaid = {}, {}, {}
models.each do |m|
#payable_po_unpaid[m.id.to_s] = PurchaseOrderx::Order.where(project_id: m.id).sum('po_total') - PaymentRequestx::PaymentRequest.where(project_id: m.id).wher
In the same file, there are fields which is much long than this and there is no problem. We have been using the export for a long time and this is first time we are losing text. What could cause the cut=off of text in CSV export?
Here is the method for CSV export, argument_value is the field with cut-off text. In debug, we verified that full context of the column has been assigned to CSV before exporting:
def self.to_csv
CSV.generate do |csv|
header = ['id', 'engine_name', 'engine_version', 'argument_name', 'argument_value', 'last_updated_by_id', 'created_at', 'updated_at', 'brief_note', 'global']
csv << header
i = 1
all.each do |config|
base = OnboardDatax.engine_config_class.find_by_id(config.engine_config_id)
row = Array.new
row << i
row << (base.global ? nil : base.engine.name)
row << base.engine_version
row << base.argument_name
row << (config.custom_argument_value.present? ? config.custom_argument_value : base.argument_value)
row << config.last_updated_by_id
row << config.created_at
row << config.updated_at
row << base.brief_note
row << (base.global ? 't' : 'f')
csv << row
i += 1
end
end
If wrapping the whole text with quotation mark, then the column can be exported to CSV in its entirety.
What could cause the cut=off of text in CSV export?
puts File.read('your_cutoff_text.txt').size
--output:--
256
Rails column types:
String:
Limited to 255 characters (depending on DBMS)
My OS automatically adds a newline to the end of a file, so your cutoff text contains exactly 255 characters.
In the same file, there are fields which is much long than this and
there is no problem.
Rails column types:
Text:
Unlimited length (depending on DBMS)

Rails: adding to last object if conditions met

I've got a method that scans an HTML string and sort of formats it for prawnpdf:
def format_for_prawn(pdf, string, colour)
body = Nokogiri::HTML::DocumentFragment.parse(string)
result = body.xpath('./*|./text()')
result.each do |breaker|
if breaker.name == "h3"
pdf.fill_color colour
pdf.text breaker.text.to_s, :size => 16
pdf.move_down 5
else
pdf.fill_color '#444444'
pdf.text breaker.text.to_s, :size => 10, :leading => 1
pdf.move_down 10
end
end
end
It works great for <h3>s. In the event that some mid-paragraph <b> (or similar) tags are found it starts a new paragraph because that's where Nokogiri broke the string--which is the correct behaviour.
How could I add the bolded string to the last pdf.text function instead of calling a new pdf.text which results in a new paragraph?
I thought about making an array out of it all but then it'll be out of order with the <h3>s.
Any help would be appreciated.
My first thought was to do a negative match :
body.xpath( './node()[not(self::b)]' )
Sadly, this would exclude <b> rather than ignoring it :
> body = Nokogiri::HTML::DocumentFragment.parse %(<h3><b>foo</b></h3><h3>bar</h3>fooz<b>baz</b>whatever); true
> body.xpath( './node()[not(self::b)]' ).to_a
[
[0] <h3>
<b>foo</b>
</h3>,
[1] <h3>bar</h3>,
[2] fooz,
[3] whatever
]
So, you'll have no choice but using a buffer, here : we can iterate through nodes first, to populate a buffer regarding if we should have a new line or not, then iterate this buffer to have your lines added to pdf :
buffer = []
body.xpath( './node()' ).each do |node|
if %w[text b].include? node.name
# add to previous line or create one
buffer << [] unless buffer.count
buffer.last << { node: node }
else
# set content and create a new line
buffer << [ { node: node, title: node.name == 'h3' } ]
buffer << []
end
end
# Now, each first level item in buffer is a line,
# containing elements we just have to concatenate text of
# to pass to `pdf#text`
buffer.each do |line|
text = line.map do |part|
node = part[ :node ]
inner = node.text.to_s
# restore <b> tag if you want bold style in pdf
node.name == 'b' ? "<b>#{inner}</b>" : inner
end.join
if line.first
if line.first[ :title ]
pdf.fill_color colour
pdf.text text, :size => 16
pdf.move_down 5
else
pdf.fill_color '#444444'
# inline_format ensure basic html formating is used, <b> in our case
# See http://prawn.majesticseacreature.com/docs/0.11.1/Prawn/Text.html#method-i-text
pdf.text text, size: 10, leading: 1, inline_format: true
pdf.move_down 10
end
end
end
Of course, all of this is considering you do not control original html. Else, you should place your text nodes inside <p> or something, and there would not be problems anymore.

Extracting hash value from a multi-dimenisonal array?

I am parsing HTML into an array as shown below. I can get the data into the array and then push the data into the database. One row of the array data is:
{:address=>"6222 Lodgepole Dr", :members=>["Diana L Dillard", "Kemberly J Williams", "George S Williams Iii"]}
The database result is:
Master Neighbor Name
7545 XYZ Dr --- - 8283 Southern Watch Pl --- - - Diana L Dillard - Kemberly J Williams - George S Williams Iii
These are my questions:
How do I get the Address.create loop to only insert the first member from the array, in this example Diana L Dillard, into the "name" field instead of all the names? Ideally I want to create one field for the first and then another for all others.
Why do I get these dashes "---" inserted with my data? I do not want them.
This is my current code:
url = "XYZ"
doc = Nokogiri::HTML(open(url))
results = []
# parse .single tags
doc.css('.single').each do |single_div|
res = {}
res[:address] = single_div.at_css('span.address').text
res[:members] = single_div.css('li.basic_info').collect{|el| el.text.strip}
results << res
end
results.each do|address|
puts "#{address}: #{members}"
Address.create(:neighboradd => "#{address}", :master => '7545 XYZ Dr', :name => "{members}")
end
This
results.each do |address|
puts "#{address}: #{members}"
Address.create(
:neighboradd => "#{address}",
:master => '7545 XYZ Dr',
:name => "{members}"
)
end
Should be something like
results.each do |res|
puts "#{res[:address]}: #{res[:members]}"
Address.create(
:neighboradd => res[:address],
:master => '7545 XYZ Dr',
:name => res[:members].join(', ') # this should be "#{res[members]}"? (with "#")
)
end
The --- is because you were using the whole hash for :neighboradd.
If you want only the first member:
:name => res[:members].first

There has got to be a cleaner way to do this

I have this code here and it works but there has to be a better way.....i need two arrays that look like this
[
{
"Vector Arena - Auckland Central, New Zealand" => {
"2010-10-10" => [
"Enter Sandman",
"Unforgiven",
"And justice for all"
]
}
},
{
"Brisbane Entertainment Centre - Brisbane Qld, Austr..." => {
"2010-10-11" => [
"Enter Sandman"
]
}
}
]
one for the past and one for the upcoming...the problem i have is i am repeating myself and though it works i want to clean it up ...here is my data
..
Try this:
h = Hash.new {|h1, k1| h1[k1] = Hash.new{|h2, k2| h2[k2] = []}}
result, today = [ h, h.dup], Date.today
Request.find_all_by_artist("Metallica",
:select => "DISTINCT venue, showdate, LOWER(song) AS song"
).each do |req|
idx = req.showdate < today ? 0 : 1
result[idx][req.venue][req.showdate] << req.song.titlecase
end
Note 1
In the first line I am initializing an hash of hashes. The outer hash creates the inner hash when a non existent key is accessed. An excerpt from Ruby Hash documentation:
If this hash is subsequently accessed by a key that doesn‘t correspond to a hash
entry, the block will be called with the hash object and the key, and should
return the default value. It is the block‘s responsibility to store the value in
the hash if required.
The inner hash creates and empty array when the non existent date is accessed.
E.g: Construct an hash containing of content as values and date as keys:
Without a default block:
h = {}
list.each do |data|
h[data.date] = [] unless h[data.date]
h[data.date] << data.content
end
With a default block
h = Hash.new{|h, k| h[k] = []}
list.each do |data|
h[data.date] << data.content
end
Second line simply creates an array with two items to hold the past and future data. Since both past and the present stores the data as Hash of Hash of Array, I simply duplicate the value.
Second line can also be written as
result = [ h, h.dup]
today = Date.today

Resources