Data structure to describe multiple boolean values as keys - ruby

I have javascript weather classification app which I am porting to ruby.
The classifications are stored in a json as
"classification": [
{
"warm": false,
"sunny": false,
"windy": false,
"desc": "cold-drizzle"
},
{
"warm": false,
"sunny": false,
"windy": true,
"desc": "storm"
},
{
"warm": false,
"sunny": true,
"windy": false,
"desc": "winter-wonderland"
}, etc
There are with other words eight different classifications.
What would be a concise ruby way to describe this structure? Perhaps a hash with multiple booleans as key?

Hashes in Ruby can be indexed by anything, including other hashes. So:
classifications = {}
classifications[ {warm:false, sunny:false, windy:false} ] = "cold-drizzle"
classifications[ {warm:false, sunny:true, windy:false} ] = "winter-wonderland"
p classifications
#=> {
#=> {:warm=>false, :sunny=>false, :windy=>false}=>"cold-drizzle",
#=> {:warm=>false, :sunny=>true, :windy=>false}=>"winter-wonderland"
#=> }
p classifications[ {sunny:false, windy:false, warm:false } ]
#=> "cold-drizzle"
Note the arbitrary ordering I used for the hash keys when reading the value. You don't have to use the exact same hash object you used when writing (as you would with Lua), you simply need to use hash whose data is equivalent.
Due to the syntax sugar of Ruby's method calls, you can even leave off all the { and } in the above (except where the hash is declared):
classifications[ warm:false, sunny:false, windy:true ] = "stormy"
p classifications[ warm:false, sunny:false, windy:false ]
#=> "cold-drizzle"
If you want to be more terse but less descriptive, you can also just use arrays for your keys:
classifications = {}
classifications[ [false,false,false] ] = "cold-drizzle"
classifications[ [false,true, false] ] = "winter-wonderland"
p classifications[ [false,true,false] ]
#=> "winter-wonderland"
Note that—as with the hashes as keys—you do not need to use the exact same object to index the hash, just an object that is considered equal.

Exactly like the JSON (array of objects):
You could use the Struct class to replicate the objects in your JSON
classifications = []
Classification = Struct.new(:warm, :sunny, :windy, :desc)
classifications << Classification.new(false, false, false, "cold_drizzle")
You could also just have an array of hashes:
classifications = []
classifications << {warm: false, sunny: false, windy: false, desc: "cold_drizzle"}
Or, if you're looking for a hash with the three booleans as a key to the description:
You could use Struct again:
classifcations = {}
Classification = Struct.new(:warm, :sunny, :windy)
classifications[Classification.new(false, false, false)] = "cold_drizzle"
You could use plain integers:
classifications = {}
classifications[0] = "cold_drizzle"
classifications[1] = "storm"
You could use arrays:
classifications = {}
classifications[[false, false, false]] = "cold_drizzle"
Or anything else you like. This question is a little open-ended, but there are some ideas.

Use JSON#parse
Given a valid JSON string, you can just parse it into a valid Ruby hash with JSON#parse. For example:
require 'json'
json_string = <<EOF
{"classification": [{
"warm": false,
"sunny": false,
"windy": false,
"desc": "cold-drizzle"
}, {
"warm": false,
"sunny": false,
"windy": true,
"desc": "storm"
}, {
"warm": false,
"sunny": true,
"windy": false,
"desc": "winter-wonderland"
}]}
EOF
hash = JSON.parse(json_string)
You can then access your new Hash object with an assortment of methods. For example:
hash['classification'].first
#=> {"warm"=>false, "sunny"=>false, "windy"=>false, "desc"=>"cold-drizzle"}

Related

Merge multiple hashes after new key

I've got three hashes which I want to merge to base_options under new key - checks. Basically what I want to achieve is:
{
base_options,
checks: {
document_check,
identity_check,
dummy_check,
}
},
Below sample hash data:
dummy_check = {
dummy: {
enabled: true,
preferences: {
state: 0,
replay: true,
},
}
}
identity_check = {
identity: {
enabled: true,
preferences: {},
},
}
document_check = {
document: {
enabled: true,
preferences: {
face: false,
liveness: false,
docs_all: true,
},
},
}
base_options = {
send_email: true,
send_reminder: false,
reset_client_status: true,
}
So if I do base_options.merge!(checks: document_check.merge!(identity_check, dummy_check)) I will receive expected hash which is:
{
send_email: true,
send_reminder: false,
reset_client_status: true,
checks: {
document: {
...
},
identity: {
...
},
dummy: {
...
}
},
}
But this is not super flexible and I don't know if using .merge! two times in one line is not a crap. Are there any other alternatives?
I'm using Ruby 2.7 and Rails 6
Using merge! is fine and well understood. However, as you are setting a single key on your base_options hash, you can also simple use the hash accessor, i.e.
base_options[:checks] = document_check.merge!(identity_check, dummy_check)
Note that this will also change the document_hash object as merge! modified the receiver. If this is not desired, you can also use merge and return a new Hash. Thus could look like:
base_options[:checks] = document_check.merge(identity_check, dummy_check)
or equivalently
base_options[:checks] = {}.merge!(document_check, identity_check, dummy_check)
The latter option is slightly slower but might better show your intended behavior and is thus easier to understand to readers of your code.
If I understand correctly, you can try the Double Splat **. you can use like this:
base_options.merge(
checks: **document_check, **identity_check, **dummy_check
)
The answer is in your question, below can be a simple way of achieving the end result.
Below is after initializing values of base_options, document_check, identity_check,dummy check.
base_options = {
base_options: base_options,
checks: {
document_check: document_check,
identity_check: identity_check,
dummy_check: dummy_check,
}
}
=> {:base_options=>{:send_email=>true, :send_reminder=>false, :reset_client_status=>true}, :checks=>{:document_check=>{:document=>{:enabled=>true, :preferences=>{:face=>false, :liveness=>false, :docs_all=>true}}}, :identity_check=>{:identity=>{:enabled=>true, :preferences=>{}}}, :dummy_check=>{:dummy=>{:enabled=>true, :preferences=>{:state=>0, :replay=>true}}}}}

DRY Strategy for looping over unknown levels of nested objects

My scenario is based on Gmail API.
I've learned that email messages can have their message parts deeply or shallowly nested based upon varying factors, but mostly the presence of attachments.
I'm using the Google API Ruby Client gem, so I'm not working with JSON, I'm getting objects with all the same information, but I think the JSON representation makes it easier to understand my issue.
A simple message JSON response looks like this (one parts array with 2 hashes inside it):
{
"id": "175b418b1ff69896",
"snippet": "COVID-19: Resources to help your business manage through uncertainty 20 Liters 500 PEOPLE FOUND YOU ON GOOGLE Here are the top search queries used to find you: 20 liters used by 146 people volunteer",
"payload": {
"parts": [
{
"mimeType": "text/plain",
"body": {
"data": "Hey, you found the body of the email! I want this!"
}
},
{
"mimeType": "text/html",
"body": {
"data": "<div>I actually don't want this</div>"
}
}
]
}
}
The value I want is not that hard to get:
response.payload.parts.each do |part|
#body_data = part.body.data if part.mime_type == 'text/plain'
end
BUT The JSON response of a more complex email message with attachments looks something like this (now parts nests itself 3 levels deep):
{
"id": "175aee26de8209d2",
"snippet": "snippet text...",
"payload": {
"parts": [
{
"mimeType": "multipart/related",
"parts": [
{
"mimeType": "multipart/alternative",
"parts": [
{
"mimeType": "text/plain",
"body": {
"data": "hey, you found me! This is what I want!!"
}
},
{
"mimeType": "text/html",
"body": {
"data": "<div>I actually don't want this one.</div>"
}
}
]
},
{
"mimeType": "image/jpeg"
},
{
"mimeType": "image/png"
},
{
"mimeType": "image/png"
},
{
"mimeType": "image/jpeg"
},
{
"mimeType": "image/png"
},
{
"mimeType": "image/png"
}
]
},
{
"mimeType": "application/pdf"
}
]
}
}
And looking at a few other messages, the object can vary from 1 to 5 levels (maybe more) of parts
I need to loop over an unknown number of parts and then loop over an unknown number of nested parts and the repeat this again until I reach the bottom, hopefully finding the thing I want.
Here's my best attempt:
def trim_response(response)
# remove headers I don't care about
response.payload.headers.keep_if { |header| #valuable_headers.include? header.name }
# remove parts I don't care about
response.payload.parts.each do |part|
# parts can be nested within parts, within parts, within...
if part.mime_type == #valuable_mime_part && part.body.present?
#body_data = part.body.data
break
elsif part.parts.present?
# there are more layers down
find_body(part)
end
end
end
def find_body(part)
part.parts.each do |sub_part|
if sub_part.mime_type == #valuable_mime_part && sub_part.body.present?
#body_data = sub_part.body.data
break
elsif sub_part.parts.present?
# there are more layers down
######### THIS FEELS BAD!!! ###########
find_body(sub_part)
end
end
end
Yep, there's a method calling itself. I know, that's why I'm here.
This does work, I've tested it on a few dozen messages, but... there has to be a better, DRY-er way to do this.
How do I recursively loop and then move down a level and loop again in a DRY fashion when I don't know how deep the nesting goes?
No need to go through all this pain. Just keep diving in the parts dictionary until you find the first value where there is no parts anymore. At this moment you have the final parts in your parts variable.
Code:
reponse = {"id" => "175aee26de8209d2","snippet" => "snippet text...","payload" => {"parts" => [{"mimeType" => "multipart/related","parts" => [{"mimeType" => "multipart/alternative","parts" => [{"mimeType" => "text/plain","body" => {"data" => "hey, you found me! This is what I want!!"}},{"mimeType" => "text/html","body" => {"data" => "<div>I actually don't want this one.</div>"}}]},{"mimeType" => "image/jpeg"}]},{"mimeType" => "application/pdf"}]}}
parts = reponse["payload"]
parts = (parts["parts"].send("first") || parts["parts"]) while parts["parts"]
data = parts["body"]["data"]
puts data
Output:
hey, you found me! This is what I want!!
You can compute the desired result using recursion.
def find_it(h, top_key, k1, k2, k3)
return nil unless h.key?(top_key)
recurse(h[top_key], k1, k2, k3)
end
def recurse(h, k1, k2, k3)
return nil unless h.key?(k1)
h[k1].each do |g|
v = g.dig(k2,k3) || recurse(g, k1 , k2, k3)
return v unless v.nil?
end
nil
end
See Hash#dig.
Let h1 and h2 equal the two hashes given in the example1. Then:
find_it(h1, :payload, :parts, :body, :data)
#=> "Hey, you found the body of the email! I want this!"
find_it(h2, :payload, :parts, :body, :data)
#=> "hey, you found me! This is what I want!!"
1. The hash h[:payload][:parts].last #=> { "mimeType": "application/pdf" } appears to contain hidden characters that are causing a problem. I therefore removed that hash from h2.

I have json data i need search `unique` if key exist or not

I have JSON data I need search unique if the key exists or not.
[
{
"key1" => []
},
{
"key" => []
},
{
"unique" => []
}
]
I can use loop but need an efficient way to check unique exist or not
You'll need to iterate through the array either way.
# You'll get found item or `nil`
data.find { |item| item.key?('unique') }
# You'll get `true` or `false`
data.any? { |item| item.key?('unique') }
Btw better to use a hash as an input instead of an array:
data = {
"key1" => [],
"key" => [],
"unique" => []
}
data.key?('unique')
=> true

Iterate and search a JSON array for the element in the array

I have a JSON array that looks like this:
response = {
"items"=>[
{
"tags"=>[
"random"
],
"timestamp"=>12345,
"storage"=>{
"url"=>"https://example.com/example",
"key"=>"mykeys"
},
"envelope"=>{
},
"log-level"=>"info",
"id"=>"random_id_test_1",
"campaigns"=>[
],
"user-variables"=>{
},
"flags"=>{
"is-test-mode"=>false
},
"message"=>{
"headers"=>{
"to"=>"random#example.com",
"message-id"=>"foobar#example.com",
"from"=>"noreply#example.com",
"subject"=>"new subject"
},
"attachments"=>[
],
"recipients"=>[
"result#example.com"
],
"size"=>4444
},
"event"=>"stored"
},
{
"tags"=>[
"flowerPower"
],
"timestamp"=>567890,
"storage"=>{
"url"=>"https://yahoo.com",
"key"=>"some_really_cool_keys_go_here"
},
"envelope"=>{
},
"log-level"=>"info",
"id"=>"some_really_cool_ids_go_here",
"campaigns"=>[
],
"user-variables"=>{
},
"flags"=>{
"is-test-mode"=>false
},
"message"=>{
"headers"=>{
"to"=>"another_great#example.com",
"message-id"=>"email_id#example.com",
"from"=>"from#example.com",
"subject"=>"email_looks_good"
},
"attachments"=>[
],
"recipients"=>[
"example#example.com"
],
"size"=>2222
},
"event"=>"stored"
}]
}
I am trying to obtain the "storage" "url" based on the "to" email.
How do I iterate through this array where x is just the element in the array
response['items'][x]["message"]["headers"]["to"]
Once I find the specific email that I need, it will stop and return the value of x which is the element number.
I was going to use that value for x and call response['items'][x]['storage']['url']
which will return the string for the URL.
I thought about doing this but there's gotta be a better way:
x = 0
user_email = another_great#example.com
while user_email != response['items'][x]["message"]["headers"]["to"] do
x+=1
value = x
puts value
end
target =
response['items'].detect do |i|
i['message']['headers']['to'] == 'another_great#example.com'
end
then
target['storage']['url']
This is another option by creating Hash with key of to's email. And on basis of it fetch required information like this:
email_hash = Hash.new
response["items"].each do |i|
email_hash[i["message"]["headers"]["to"]] = i
end
Now if you want to fetch "storage" "url" then simply do:
user_email = "another_great#example.com"
puts email_hash[user_email]["storage"]["url"] if email_hash[user_email]
#=> "https://yahoo.com"
You can use it as #Satoru suggested. As a suggestion, if you use case involves complex queries on json data (more complex than this), then you can store your data in mongodb, and can elegantly query anything.

Delete nested hash according to key => value

I have this hash:
response = '{"librairies":[{"id":1,"books":[{"id":1,"qty":1},{"id":2,"qty":3}]},{"id":2,"books":[{"id":1,"qty":0},{"id":2,"qty":3}]}]}'
in which I'd like to delete every librairies where, at least, one of the book quantity is null.
For instance, with this given response, I'd expect this return:
'{"librairies":[{"id":1,"books":[{"id":1,"qty":1},{"id":2,"qty":3}]}]}'
I've tried this:
parsed = JSON.parse(response)
parsed["librairies"].each do |library|
library["books"].each do |book|
parsed.delete(library) if book["qty"] == 0
end
end
but this returns the exact same response hash, without having deleted the second library (the one with id => 2).
You can use Array#delete_if and Enumerable#any? for this
# Move through each array element with delete_if
parsed["librairies"].delete_if do |library|
# evaluates to true if any book hash in the library
# has a "qty" value of 0
library["books"].any? { |book| book["qty"] == 0 }
end
Hope this helps
To avoid changing the hash parsed, you could do the following.
Firstly, let's format parsed so we can see what we're dealing with:
parsed = { "libraries"=>[ { "id"=>1,
"books"=>[ { "id"=>1, "qty"=>1 },
{ "id"=>2, "qty"=>3 } ]
},
{ "id"=>2,
"books"=>[ { "id"=>1, "qty"=>0 },
{ "id"=>2, "qty"=>3 } ]
}
]
}
Later I want to show that parsed has not been changed when we create the new hash. An easy way of doing that is to compute a hash code on parsed before and after, and see if it changes. (While it's not 100% certain that different hashes won't have the same hash code, here it's not something to lose sleep over.)
parsed.hash
#=> 852445412783960729
We first need to make a "deep copy" of parsed so that changes to the copy will not affect parsed. One way of doing that is to use the Marshal module:
new_parsed = Marshal.load(Marshal.dump(parsed))
We can now modify the copy as required:
new_parsed["libraries"].reject! { |h| h["books"].any? { |g| g["qty"].zero? } }
#=> [ { "id"=>1,
# "books"=>[ { "id"=>1, "qty"=>1 },
# { "id"=>2, "qty"=>3 }
# ]
# }
# ]
new_parsed # => { "libraries"=>[ { "id"=>1,
"books"=>[ { "id"=>1, "qty"=>1},
{ "id"=>2, "qty"=>3}
]
}
]
}
And we confirm the original hash was not changed:
parsed.hash
#=> 852445412783960729

Resources