I have a string "Animals ( Reptiles Birds ( Eagles Pigeons Crows ) )" and I need to return:
a = [
{
"Animals" => [
{
"Reptiles" => nil
},
{
"Birds" => [
{ "Eagles" => nil },
{ "Pigeons" => nil },
{ "Crows" => nil }
]
}
]
}
]
I don't understand how I can do it.
Where I can find some example or what I can search in google?
Here is a one way you could convert the string to an array.
Code
def arrayify(arr)
a = split_arr(arr)
a.map do |h|
k = h.keys.first
v = h.values.first
case v
when Array then { k => arrayify(v) }
else { k=>v }
end
end
end
def split_arr(arr)
a = []
while arr.any?
word = arr.shift
if arr.empty? || arr.first != ?(
a << { word=>nil }
else
arr.shift
close_count = 0
b = []
loop do
token = arr.shift
case token
when ?)
break if close_count == 0
close_count -= 1
when ?( then close_count += 1
end
b << token
end
a << { word=>b }
end
end
a
end
Example
str = "Animals ( Reptiles Birds ( Eagles Pigeons Crows ) ) Foods ( " +
"Snacks Breakfast ( Pancakes Waffles ) )"
arrayify(str.split)
#=> [{"Animals"=>[{"Reptiles" =>nil},
# {"Birds" =>[{"Eagles" =>nil},
# {"Pigeons"=>nil},
# {"Crows" =>nil}
# ]
# }
# ]
# },
# {"Foods" =>[{"Snacks" =>nil},
# {"Breakfast"=>[{"Pancakes"=>nil},
# {"Waffles" =>nil}
# ]
# }
# ]
# }
# ]
I don't understand how I can do it. Where I can find some example or what I can search in google?
Using a recursive regex is one option, especially if the parenthesis are properly balanced:
http://www.regular-expressions.info/recurse.html
If it's over your head, recursively plough through the string using a normal regex. Match something like:
[a-z]+ ?([^()]*)
... then replace the match with a place holder in the original string. Rinse, repeat.
Using a parser is an alternative option. You could write a simplistic one, or use a tool e.g.:
http://thingsaaronmade.com/blog/a-quick-intro-to-writing-a-parser-using-treetop.html
This works with your example, but I don't know how general it is.
Code
def arrayify(str)
eval('['+str.gsub(/(\w+)\s+\(/,'{\1=>[')
.gsub( /(?!\{)(\w+)\s+/, '{\1=>nil},')
.gsub(')', ']}')
.gsub(/\b(\w+)\b/,"\"\\1\"")+']')
end
Example
str = "Animals ( Reptiles Birds ( Eagles Pigeons Crows ) )"
arrayify(str)
#=> [{ "Animals"=>[{ "Reptiles"=>"nil"},
# { "Birds" =>[{ "Eagles" =>"nil" },
# { "Pigeons"=>"nil" },
# { "Crows" =>"nil" }
# ]
# }
# ]
# }
# ]
Explanation
s1 = str.gsub(/(\w+)\s+\(/,'{\1=>[')
#=> "{Animals=>[ Reptiles {Birds=>[ Eagles Pigeons Crows ) )"
s2 = s1.gsub( /(?!\{)(\w+)\s+/, '{\1=>nil},')
#=> "{Animals=>[ {Reptiles=>nil},{Birds=>[ {Eagles=>nil},{Pigeons=>nil},{Crows=>nil},) )"
s3 = s2.gsub(')', ']}')
#=> "{Animals=>[ {Reptiles=>nil},{Birds=>[ {Eagles=>nil},{Pigeons=>nil},{Crows=>nil},]} ]}"
s4 = s3.gsub(/\b(\w+)\b/,"\"\\1\"")
#=> "{\"Animals\"=>[ {\"Reptiles\"=>\"nil\"},{\"Birds\"=>[ {\"Eagles\"=>\"nil\"},{\"Pigeons\"=>\"nil\"},{\"Crows\"=>\"nil\"},]} ]}"
eval('['+s4+']')
#=> <result in example>
Pardon me, but I have to run. The eval police are coming.
Related
I want sort an array of String by alphabet but force one to the front of the array
p ['Home', 'Contact', 'Profile', 'Jobs', 'Privacy'].sort { |x, y|
if x == 'Privacy'
-1
else
x <=> y
end
}
Gives me ["Contact", "Home", "Jobs", "Privacy", "Profolio"]
instead of ["Privacy", "Contact", "Home", "Jobs", "Profolio"]
Eventually I want to sort an array of objects by name attribute
class Page
include Comparable
def <=> (other)
if self.name == 'Jobs'
-1
else
self.name <=> other.name
end
end
end
[Page.new('Contact'), Page.new('Home'), Page.new('Jobs')...].sort
However overwriting <=> in the Comparable class doesn't work as well
You just need to specify the sort for both x and y:
["Home", "Contact", "Profile", "Jobs", "Privacy"].sort { |x, y|
if x == "Privacy" # "Privacy" <=> y
-1 # y is lower
elsif y == "Privacy" # x <=> "Privacy"
1 # "Privacy" is higher
else # the rest
x <=> y # use default sort order
end
}
# => ["Privacy", "Contact", "Home", "Jobs", "Profile"]
Same thing when using Comparable:
class Page
include Comparable
def initialize name
#name = name
end
def <=> other
if #name == "Privacy"
-1
elsif other == "Privacy"
1
else
#name <=> other
end
end
end
>> [Page.new("Contact"), Page.new("Home"), Page.new("Jobs"), Page.new("Privacy")].sort
=> [#<Page:0x00007f8688624d30 #name="Privacy">, #<Page:0x00007f8688624ec0 #name="Contact">, #<Page:0x00007f8688624e20 #name="Home">, #<Page:0x00007f8688624da8 #name="Jobs">]
One other way I know:
>> ["Home", "Contact", "Profile", "Jobs", "Privacy"]
.partition{|i| i == "Privacy"}
.flat_map(&:sort)
=> ["Privacy", "Contact", "Home", "Jobs", "Profile"]
Ok, I was just wondering:
require "benchmark"
a = (1..100_000).map{ rand(36**8).to_s(36) } + ["privacy"]
d = a.dup # because `.delete` modifies the array
Benchmark.bm(15) do |b|
b.report("Delete Sort") { [d.delete("privacy"), *d.sort] }
b.report("Partition Sort") { a.partition{|i| i == "privacy"}.flat_map(&:sort) }
b.report("Sort by String") { a.sort_by {|i| i == "privacy" ? "" : i } }
b.report("Sort") { a.sort {|x, y| if x == "privacy";-1; elsif y == "privacy";1; else x<=>y end } }
b.report("Sort by Array") { a.sort_by {|s| s == "privacy" ? [0, ""] : [1, s] } }
end
user system total real
Delete Sort 0.035848 0.000000 0.035848 ( 0.035891)
Partition Sort 0.045608 0.000044 0.045652 ( 0.045711)
Sort by String 0.068656 0.000018 0.068674 ( 0.068733)
Sort 0.307052 0.000000 0.307052 ( 0.307237)
Sort by Array 0.493351 0.000000 0.493351 ( 0.493606) # making arrays is expensive
Try the following.
arr = ['Home', 'Contact', 'Profile', 'Jobs', 'Privacy']
arr.sort_by { |s| s == 'Privacy' ? [0, ''] : [1, s] }
#=> ["Privacy", "Contact", "Home", "Jobs", "Profile"].
Enumerable#sort_by uses Array#<=> to order arrays. See especially the third paragraph of the latter doc.
Note that the second element of [0, ''] is arbitrary. That's because it is never referenced by sort_by. For example, [0, { a:1, b:2 }] or [0] would work as well.
It's probably faster to use sort_by than sort because sort_by computes a two-element array for each element of the array only once, whereas sort performs a comparable calculation twice for each pairwise ordering it considers.
Better, in a comment #Alex suggested:
arr.sort_by { |s| s == "Privacy" ? "" : s }
Alternatively, you could write:
a = arr.sort
[a.delete('Privacy'), *a]
#=> ["Privacy", "Contact", "Home", "Jobs", "Profile"]
I have a array like
array = [
{"point"=>6, "score"=>4, "team"=>"Challenger"},
{"point"=>4, "score"=>2, "team"=>"INB"},
{"point"=>2, "score"=>2, "team"=>"Super-11"},
{"point"=>3, "score"=>7, "team"=>"INB"}
]
I want to merge hashes by "team" and sum the values of "point" and "score". Additionally want to insert an key "qualified" in each hash if point is greater than 5. So the final result will be:
result= [
{"point"=>6, "score"=>4, "qualified"=> "yes", "team"=>"Challenger"},
{"point"=>7, "score"=>9, "qualified"=> "yes", "team"=>"INB"},
{"point"=>2, "score"=>2, "qualified"=> "no", "team"=>"Super-11"}
]
Any help would be appreciated. Thanks!
One more possible solution :)
array.group_by { |item| item['team'] }.map do |_, items|
result = items.inject({}) { |hash, item| hash.merge(item) { |_, old, new| Integer(old) + new rescue old } }
result.merge("qualified" => result['point'] > 5 ? "yes" : "no")
end
Combination of group_by and map should help
result =
array.group_by {|item| item['team'] }
.map do |team, items|
total_points = items.map{|item| item['point']}.reduce(0, :+)
total_score = items.map{|item| item['score']}.reduce(0, :+)
qualified = points > 5
{
'point' => total_points,
'score' => total_score,
'qualified' => qualified ,
'team' => team
}
end
result = array.group_by{|i| i['team']}
.map do |k,v|
points = v.map{|i| i['point']}.inject(0, :+)
score = v.map{|i| i['score']}.inject(0, :+)
{
'point' => points,
'score' => score,
'qualified' => points > 5 ? 'yes' : 'no',
'team' => k
}
end
This is an alternative version. group_by is mandatory, I guess.
I used a temporary hash with keys as symbol to store data during iterations.
result = array.group_by { |hash| hash['team'] }.map do |team|
tmp_hash = {point: 0, score: 0, team: team[0], qualified: 'no'}
team[1].each { |h| tmp_hash[:point] += h['point'] ; tmp_hash[:score] += h['score'] }
tmp_hash[:qualified] = 'yes' if tmp_hash[:point] > 5
tmp_hash
end
this gives as result:
# => [
# {:point=>6, :score=>4, :team=>"Challenger", :qualified=>"yes"},
# {:point=>7, :score=>9, :team=>"INB", :qualified=>"yes"},
# {:point=>2, :score=>2, :team=>"Super-11", :qualified=>"no"}
# ]
After doing group_by, a simple map operation which takes the first element as the mapped value, sums up point and score within it and then merges the qualified condition into it is easy enough:
array
.group_by { |h| h["team"] }
.map do |_, a|
["point", "score"].each { |k| a.first[k] = a.sum { |h| h[k] } }
a.first.merge({"qualified": a.first["score"] > 5 ? 'yes' : 'no'})
end
Online demo here
array.each_with_object({}) do |g,h|
h.update(g["team"]=>g.merge("qualified"=>g["score"] > 5 ? "yes" : "no")) do |_,o,n|
{ "point" =>o["point"]+n["point"],
"score" =>o["score"]+n["score"],
"team" =>o["team"],
"qualified"=>(o["score"]+n["score"]) > 5 ? "yes" : "no" }
end
end.values
#=> [{"point"=>6, "score"=>4, "team"=>"Challenger", "qualified"=>"no"},
# {"point"=>7, "score"=>9, "team"=>"INB", "qualified"=>"yes"},
# {"point"=>2, "score"=>2, "team"=>"Super-11", "qualified"=>"no"}]
This uses the form of Hash#update (aka merge!) that employs a block to determine the values of keys (here the value of :id) that are present in both hashes being merged. See the doc for the description of the three block variables (here _, o and n).
Note that the receiver of values (at the end) is
{"Challenger"=>{"point"=>6, "score"=>4, "team"=>"Challenger", "qualified"=>"no"},
"INB"=>{"point"=>7, "score"=>9, "team"=>"INB", "qualified"=>"yes"},
"Super-11"=>{"point"=>2, "score"=>2, "team"=>"Super-11", "qualified"=>"no"}}
One could alternatively make a separate pass at the end to add the key "qualified':
array.each_with_object({}) do |g,h|
h.update(g["team"]=>g) do |_,o,n|
{ "point" =>o["point"]+n["point"],
"score" =>o["score"]+n["score"],
"team" =>o["team"] }
end
end.values.
map { |h| h.merge("qualified"=>(h["score"] > 5) ? "yes" : "no") }
str = "<a><b><c></c></b></a>"
hash = Hash.from_xml(str)
# => {"a"=>{"b"=>{"c"=>nil}}}
How can I replace all nils in a Hash to "" so that the hash becomes:
{"a"=>{"b"=>{"c"=>""}}}
Here is a recursive method that does not change the original hash.
Code
def denilize(h)
h.each_with_object({}) { |(k,v),g|
g[k] = (Hash === v) ? denilize(v) : v.nil? ? '' : v }
end
Examples
h = { "a"=>{ "b"=>{ "c"=>nil } } }
denilize(h) #=> { "a"=>{ "b"=>{ "c"=>"" } } }
h = { "a"=>{ "b"=>{ "c"=>nil , "d"=>3, "e"=>nil}, "f"=>nil } }
denilize(h) #=> { "a"=>{ "b"=>{ "c"=>"" , "d"=>3, "e"=>""}, "f"=>"" } }
this will destroy the original hash and will not work with hashes with infinite recursion.
def nil2empty(hash)
hash.keys.each do |key|
if hash[key].kind_of? Hash
nil2empty(hash[key])
else
hash[key] = '' if hash[key].nil?
end
end
true # of course, what else? :P
end
example of usage:
hash
=> {"a"=>{"b"=>{"c"=>nil}}}
nil2empty(hash)
=> true
hash
=> {"a"=>{"b"=>{"c"=>""}}}
I know this is not the answer you are expecting, but if you could handle a value instead of "" , this code works
eval({"a"=>{"b"=>{"c"=>nil}}}.to_s.gsub("nil", "1")) #=> returns a hash #{"a"=>{"b"=>{"c"=>1}}}
Right now, I'm merging two hashes like this:
department_hash = self.parse_department html
super_saver_hash = self.parse_super_saver html
final_hash = department_hash.merge(super_saver_hash)
Output:
{:department=>{"Pet Supplies"=>{"Birds"=>16281, "Cats"=>245512,
"Dogs"=>513926, "Fish & Aquatic Pets"=>46811, "Horses"=>14805,
"Insects"=>364, "Reptiles & Amphibians"=>5816, "Small
Animals"=>19769}}, :super_saver=>{"Free Super Saver
Shipping"=>126649}}
But now I want to merge more in the future. For example:
department_hash = self.parse_department html
super_saver_hash = self.parse_super_saver html
categories_hash = self.parse_categories html
How to merge multiple hashes?
How about:
[department_hash, super_saver_hash, categories_hash].reduce &:merge
You can just call merge again:
h1 = {foo: :bar}
h2 = {baz: :qux}
h3 = {quux: :garply}
h1.merge(h2).merge(h3)
#=> {:foo=>:bar, :baz=>:qux, :quux=>:garply}
You can do below way using Enumerable#inject:
h = {}
arr = [{:a=>"b"},{"c" => 2},{:a=>4,"c"=>"Hi"}]
arr.inject(h,:update)
# => {:a=>4, "c"=>"Hi"}
arr.inject(:update)
# => {:a=>4, "c"=>"Hi"}
It took me a while to figure out how to merge multi-nested hashes after going through this Question and its Answers. It turned out I was iterating through the collections of hashes incorrectly, causing all kinds of problems with null values.
This sample command-line app shows how to merge multiple hashes with a combination of store and merge!, depending on whether or not they were top-level hash keys. It uses command-line args with a few known key name for categorization purposes.
Full code from the Gist URL is provided below as a courtesy:
# Ruby - A nested hash example
# Load each pair of args on the command-line as a key-value pair
# For example from CMD.exe:
# call ruby.exe ruby_nested_hash_example.rb Age 30 Name Mary Fav_Hobby Ataraxia Fav_Number 42
# Output would be:
# {
# "data_info": {
# "types": {
# "nums": {
# "Age": 30,
# "Fav_Number": 42
# },
# "strings": {
# "Name": "Mary",
# "Fav_Hobby": "Ataraxia"
# }
# },
# "data_id": "13435436457"
# }
# }
if (ARGV.count % 2 != 0) || (ARGV.count < 2)
STDERR.puts "You must provide an even amount of command-line args to make key-value pairs.\n"
abort
end
require 'json'
cmd_hashes = {}
nums = {}
strings = {}
types = {}
#FYI `tl` == top-level
all_tl_keys = {}
data_info = {}
data_id = {:data_id => "13435436457"}
_key = ""
_value = ""
element = 0
ARGV.each do |i|
if element % 2 == 0
_key=i
else
if (i.to_i!=0) && (i!=0)
_value=i.to_i
else
_value=i
end
end
if (_key != "") && (_value != "")
cmd_hashes.store(_key, _value)
_key = ""
_value = ""
end
element+=1
end
cmd_hashes.each do |key, value|
if value.is_a? Numeric
nums.store(key, value)
else
strings.store(key, value)
end
end
if nums.size > 0; types.merge!(:nums => nums) end
if strings.size > 0; types.merge!(:strings => strings) end
if types.size > 0; all_tl_keys.merge!(:types => types) end
if data_id.size > 0; all_tl_keys.merge!(data_id) end
if all_tl_keys.size > 0; data_info.merge!(:data_info => all_tl_keys) end
if data_info.size > 0; puts JSON.pretty_generate(data_info) end
Suppose you are having arr = [{x: 10},{y: 20},{z: 30}]
then do
arr.reduce(:merge)
Input
cycle = 4
order = []
order[0] = [
/foobar/, /vim/
]
order[1] = [ /simple/,/word/, /.*/ ]
record = [ 'vim', 'foobar', 'foo', 'word', 'bar', 'something', 'something1', 'something2', 'something3', 'something4']
Requirement
I want to make a list named report. Original source is record which is an one-dimension array. All elements of record will be split into different group and sorted. The group and order is defined in order.
This is pseudo code:
order.each do |group|
group.each do |pattern|
record.each do |r|
if r =~ pattern
#report[# of group][# of row][ # of element (max is 4th)] = r
end
end
end
end
Please note:
the element number in a [row] is 4 which is defined in cycle.
[# of row] : If # of element > 4 , # of row will + 1
Every element(string) in report is unique.
Expected output:
require 'ap'
ap report
[
[0] [
[0] [
[0] "foobar",
[1] "vim"
]
],
[1] [
[0] [
[0] "word",
[1] "foo",
[2] "bar",
[3] "something"
],
[1] [
[0] "something1",
[1] "something2"
[2] "something3"
[3] "something4"
]
]
]
This should do it (though it's not very pretty):
report = []
record.uniq!
order.each_with_index do |group, gi|
group.each do |pattern|
record.select { |r| r =~ pattern }.each do |match|
report[gi] ||= [[]]
report[gi] << [] if report[gi].last.length == cycle
report[gi].last << match
end
record.delete_if { |r| r =~ pattern }
end
end
puts report.inspect
#=> [[["foobar", "vim"]], [["word", "foo", "bar", "something"], ["something1", "something2", "something3", "something4"]]]
Note that record is mutated, so if you need it to remain the same you should dup it.
Here's another approach. I'm still not entirely happy with this -- couldn't figure out how to boil down the last two steps into one. Also it ended up having more lines than Andrew Marshall's answer. Boo.
Spec attached.
require 'spec_helper'
def report(cycle, order, record)
record.uniq!
order.each_with_index.map do |pattern_list, index|
pattern_list.map do |pattern|
record.each_with_index.inject([]) do |memo, (item, item_index)|
memo.tap do
if pattern =~ item
memo << item
record[item_index] = nil
end
end
end
end.flatten
end.map do |items|
items.each_with_index.group_by do |item, index|
index.div(cycle)
end.map do |ordering, item_with_index|
item_with_index.map(&:first)
end
end
end
describe 'report' do
let(:cycle) { 4 }
let(:order) { [
[/foobar/, /vim/],
[/simple/,/word/, /.*/]
] }
let(:record) {
[ 'vim', 'foobar', 'foo', 'word', 'bar', 'something', 'something1', 'something2', 'something3', 'something4']
}
it "just works" do
report(cycle, order, record.dup).should == [
[["foobar","vim"]],
[["word","foo","bar","something"],["something1","something2","something3","something4"]]
]
end
end
Simple answer, you can use each_with_index which works similar to each but gives you the index if the loop as the second parameter.
I can't give you a full example sadly, as I didn't fully understand your use case. However with the documentation you should be able to proceed.