Get values from nested hash - ruby

I have this Hash that I use store values and if the values are not found to get default values:
AMOUNT = {
EUR: {
eps: { AT: 1_00 },
safetypay: { PE: 15_000_00, CR: 5_000_00, BE: 15_000_00, },
przelewy24: 5_00,
qiwi: 5_00,
bcmc: { AT: 1_00, BE: 1_00 },
giropay: { DE: 1_00 },
ideal: { NL: 1_00 },
mybank: { IT: 1_00, FR: 1_00 },
},
CZK: {
trustpay: { CZ: 20_00 }
}
}.with_indifferent_access
I would like to get values based on the keys so I tried this:
def amount_for(payment_type, country, currency)
payment_amount = AMOUNT.dig(currency, payment_type, country) if payment_type.is_a?(Hash)
payment_amount ||= AMOUNT.dig(currency, payment_type)
payment_amount ||= 1
end
But I get for result not number but {"AT"=>100, "BE"=>100}. If I remove the check if payment_type.is_a?(Hash) I get exception Integer does not have #dig method (RuntimeError)
Do you know how I can solve this issue?

payment_type will be e.g. "AT" - it's the argument you pass into your function, it will never be a Hash.
This rewrite should do what you want:
def amount_for(payment_type, country = nil, currency = nil)
path = [payment_type, country, currency].compact
obj = AMOUNT
obj = obj[path.shift] while Hash === obj && !path.empty?
return obj || 1
end
Alternately, this is rather similar to the code you wrote:
def amount_for(payment_type, country = nil, currency = nil)
tmp = AMOUNT.dig(payment_type, country, currency)
return tmp if tmp
tmp = AMOUNT.dig(payment_type, country)
return tmp if tmp
tmp = AMOUNT.dig(payment_type)
return tmp if tmp
return 1
end

Related

elasticsearch sort by price with currency

I have data
{
"id": 1000,
"price": "99,01USA",
},
{
"id": 1001,
"price": "100USA",
},
{
"id": 1002,
"price": "780USA",
},
{
"id": 1003,
"price": "20USA",
},
How I sort order by price (ASC , DESC)
You can alter it a little to parse price to integer and then sort it
You can create a dynamic sort function that sorts objects by their value that you pass:
function dynamicSort(property) {
var sortOrder = 1;
if(property[0] === "-") {
sortOrder = -1;
property = property.substr(1);
}
return function (a,b) {
/* next line works with strings and numbers,
* and you may want to customize it to your needs
*/
var result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0;
return result * sortOrder;
}
}
So you can have an array of objects like this:
var People = [
{Name: "Name", Surname: "Surname"},
{Name:"AAA", Surname:"ZZZ"},
{Name: "Name", Surname: "AAA"}
];
...and it will work when you do:
People.sort(dynamicSort("Name"));
People.sort(dynamicSort("Surname"));
People.sort(dynamicSort("-Surname"));
Actually this already answers the question. Below part is written because many people contacted me, complaining that it doesn't work with multiple parameters.
Multiple Parameters
You can use the function below to generate sort functions with multiple sort parameters.
function dynamicSortMultiple() {
/*
* save the arguments object as it will be overwritten
* note that arguments object is an array-like object
* consisting of the names of the properties to sort by
*/
var props = arguments;
return function (obj1, obj2) {
var i = 0, result = 0, numberOfProperties = props.length;
/* try getting a different result from 0 (equal)
* as long as we have extra properties to compare
*/
while(result === 0 && i < numberOfProperties) {
result = dynamicSort(props[i])(obj1, obj2);
i++;
}
return result;
}
}
Which would enable you to do something like this:
People.sort(dynamicSortMultiple("Name", "-Surname"));
Subclassing Array
For the lucky among us who can use ES6, which allows extending the native objects:
class MyArray extends Array {
sortBy(...args) {
return this.sort(dynamicSortMultiple(...args));
}
}
That would enable this:
MyArray.from(People).sortBy("Name", "-Surname");

Filter multiple values in Swift

I know that Filter and contains in Swift are very powerful and it has made many of my works easy. However, I'm stranded in a situation where I have to filter/verify a set of different values from an array of Model.
Let's assume I have a model like below
struct Person {
var name: String
var ID: String
}
And I have an array of models like below:
[
{
sku = "123"
attributes = [
{
name:"Victor"
ID:"250"
},
{
name:"Shaw"
ID:"252"
}
]
},
{
sku = "123"
attributes = [
{
name:"John"
ID:"222"
},
{
name:"Nixon"
ID:"333"
}
]
}
]
Now I would like to filter the sku by multiple IDs inside attributes . For example, if I need to filter skus which has ID:250 and ID:252 I need something like this to achieve it.
personModelArray.forEach { person in
person.attributes.contains(where: { $0.ID == 250 && $0.ID == 252 })
}
OR
personModelArray.forEach { person in
person.attributes.filter { $0.ID == 250 && $0.ID == 252 }
}
What I want is a condition like to to verify that the attributes array has both the values.
The condition $0.ID == 250 && $0.ID == 252 is ALWAYS false! How one storage could have two different values at the same moment?
Look at this simple example
struct Item {
let a: Int
let b: String
}
let arr = [Item(a: 0, b: "zero"),
Item(a: 1, b: "one"),
Item(a: 2, b: "two"),
Item(a: 4, b: "four")]
To filter all items, where a == 1 OR a == 4
let arr0 = arr.filter { item in
item.a == 1 ||
item.a == 4
}
or, if You prefer $ parameter notation
let arr1 = arr.filter { $0.a == 1 || $0.a == 4 }
eventually,
let arr2 = arr.filter { item in
[1, 4].contains(item.a)
}
let arr3 = arr.filter { [1, 4].contains($0.a) }

how to filter an array of objects

I have following array of objects
{
: items=>[
{
: id=>"cam-id-1",
: translations=>[
{
: name=>"abcd",
: description=>"geiajfe",
: locale=>: fr,
: createdAt=>Fri,
27Jul201800: 00: 00UTC+00: 00,
: updatedAt=>Fri,
27Jul201800: 00: 00UTC+00: 00
},
{
: name=>"bon jor",
: description=>"jwi nifneaoin ofieafi",
: locale=>: de,
: createdAt=>Fri,
27Jul201800: 00: 00UTC+00: 00,
: updatedAt=>Fri,
27Jul201800: 00: 00UTC+00: 00
},
{
: name=>"hello",
: description=>"hello abcd",
: locale=>: en,
: createdAt=>Fri,
27Jul201800: 00: 00UTC+00: 00,
: updatedAt=>Fri,
27Jul201800: 00: 00UTC+00: 00
}
]
},
{
: id=>"cam-id-2",
: translations=>[
]
}
],
}
I want to filter the translations array based on locale. so e.g if i pass locale = fr then return the whole object but only one object in translations since there's only one locale fr.
so the output will be
{
: items=>[
{
: id=>"cam-id-1",
: translations=>[
{
: name=>"abcd",
: description=>"geiajfe",
: locale=>: fr,
: createdAt=>Fri,
27Jul201800: 00: 00UTC+00: 00,
: updatedAt=>Fri,
27Jul201800: 00: 00UTC+00: 00
}
]
},
{
: id=>"cam-id-2",
: translations=>[
]
}
],
}
i created a method to filter translations but it just returns filtered translation and i dont think this is the right way.
def filter_translations(test)
array = []
test[:items][0][:translations].each do |t|
array << t if t[:locale].to_s.casecmp(locale.to_s).zero? || t[:locale].to_s.include?(locale.to_s)
end
array
end
any idea?
To get just a single thing from a list, use first.
# {:name=>"abcd", :description=>"geiajfe", :locale=>:fr}
translation_fr = item.fetch(:translations).first { |translation|
translation[:locale] == :fr
}
To get this for all items, wrap it in a map.
# [ {:name=>"abcd", :description=>"geiajfe", :locale=>:fr}, ... ]
items_fr = test.fetch(:items).map { |item|
item.fetch(:translations).first { |translation|
translation[:locale] == :fr
}
}
To return the item + the chosen translation, you can return this from the map. But you need to be careful, item is not a copy. If you alter it in the map you alter it in tests.
For example...
items_fr = test.fetch(:items).map { |item|
translation = item.fetch(:translations).first { |translation|
translation[:locale] == :fr
}
item[:translations] = [translation]
item
}
This will return what you want.
{:id=>"cam-id-1", :translations=>[{:name=>"abcd", :description=>"geiajfe", :locale=>:fr}]}
But it will also alter the items in test, they're the same objects. Instead you need to return a copy of each item. You can call item = item.clone and change the key, or you can use merge in one step.
items_fr = test.fetch(:items).map { |item|
translation = item.fetch(:translations).first { |translation|
translation[:locale] == :fr
}
item.merge translations: [translation]
}
Rather than altering item[:translations], I'd recommend you add a new translation key for your chosen translation.
# [ {:id=>"cam-id-1", :translations=>[{:name=>"abcd", :description=>"geiajfe", :locale=>:fr}, {:name=>"bon jor", :description=>"jwi nifneaoin ofieafi", :locale=>:de}, {:name=>"hello", :description=>"hello abcd", :locale=>:en}], :translation=>{:name=>"abcd", :description=>"geiajfe", :locale=>:fr}} ]
items_fr = test.fetch(:items).map { |item|
translation = item.fetch(:translations).first { |translation|
translation[:locale] == :fr
}
item[:translation] = translation
item
}
Now you can repeat this process to pick a new translation if necessary.
You can improve this process in a few ways. First, if possible, change the translation array to a hash keyed on locale for faster lookup. Second, get everything into objects which encapsulates the details and allows for more complex behavior.
class Translations
attr_accessor :translations
def initialize
#translations = {}
end
# Get the translation for a locale
def for_locale(locale)
translations.fetch(locale)
end
# Add a translation for a locale
def add_translation(translation)
translations[translation.locale] = translation
end
# Import translations from an array. Reorganize
# them as a hash keyed on locale.
def import_from_array(translations)
translations.each do |import_translation|
add_translation Translation.new(import_translation)
end
end
end
class Translation
attr_accessor :name, :description, :locale
def initialize(name:, description:, locale:)
#name = name
#description = description
#locale = locale
end
end
class Item
attr_accessor :translations, :translation, :id
def initialize
#translations ||= Translations.new
end
def pick_translation(locale)
#translation = translations.for_locale(locale)
end
def import_from_hash(**args)
#id = args.fetch(:id)
translations.import_from_array( args.fetch(:translations) )
end
end
items = test.fetch(:items).map do |import|
# parse
item = Item.new.import_from_hash(import)
# pick a translation
item.pick_translation(:fr)
# return the object
item
end
For example, if these translations are repeated and the copies are taking up memory you could leverage the Flyweight pattern. Rather than each Item having its own copy, they could share the same Translation objects.
I've used Hash#fetch rather than [] so it will throw a KeyError exception if the required keys are missing.

How do I use an array of named indices (keys) to set a value in a nested hash?

Given any nested hash, for example:
{ canada:
{ ontario:
{ ottawa: :me},
manitoba:
{ winnipeg: nil}},
united_states:
{ district_of_coloumbia:
{ washington: nil}}}
how can I use any array of keys [:canada, :ontario, :ottawa] or [:united_states, :district_of_columbia, :washington] to get or set a value.
Basically, my problem is how do I change [:canada, :ontario, :ottawa] into a getter or setter of the format hash[:canada][:ontario][:ottawa] when I don't know the length of the array of keys.
so I can do something like:
hash[:canada][:ontario][:ottawa] = nil
hash[:canada][:manitoba][:winnipeg] = :me
I made a getter using recursion:
def reindex(h, index_array)
i = index_array.shift
result = index_array.empty? ? h[i] : reindex(h[i], index_array)
result
end
But I feel like I'm over thinking this and there should be a simpler way.
Much simpler approach(in my opinion) is to access elements successively with :[]:
keys = [:canada, :ontario, :ottawa]
hash = { canada: { ontario: { ottawa: :me}, manitoba: { winnipeg: nil} }, united_states: { district_of_coloumbia: { washington: nil } } }
# get
p keys.inject(hash) { |h, k| h.public_send(:[], k) }
#=> :me
# set
last = keys[0..-2].inject(hash) { |h, k| h.public_send(:[], k) }
last.public_send(:[]=, keys[-1], 'other')
p hash #=> {:canada=>{:ontario=>{:ottawa=>"other"}, :manitoba=>{:winnipeg=>nil}}, :united_states=>{:district_of_coloumbia=>{:washington=>nil}}}
Wrapped in methods:
def get_by_keys(hash, keys)
keys.inject(hash) { |h, k| h.public_send(:[], k) }
end
def set_by_keys(hash, keys, v)
last = keys[0..-2].inject(hash) { |h, k| h.public_send(:[], k) }
last.public_send(:[]=, keys[-1], v)
hash
end
keys = [:canada, :ontario, :ottawa]
hash = { canada: { ontario: { ottawa: :me}, manitoba: { winnipeg: nil} }, united_states: { district_of_coloumbia: { washington: nil } } }
p get_by_keys(hash, keys) #=> :me
p set_by_keys(hash, keys, 'other') #=> {:canada=>{:ontario=>{:ottawa=>"other"}, :manitoba=>{:winnipeg=>nil}}, :united_states=>{:district_of_coloumbia=>{:washington=>nil}}}
class Hash
def deep_fetch(*path)
path.reduce(self) do |mem, key|
mem[key] if mem
end
end
def deep_assign(*path, val)
key = path.shift
if path.empty?
self[key] = val
else
if self[key].is_a?(Hash)
self[key].deep_assign(*path, val)
else
self[key] = path.reverse.inject(val) { |a, n| {n => a} }
end
end
self
end
end
countries = {:canada=>{:ontario=>{:ottawa=>:me}, :manitoba=>{:winnipeg=>nil}}, :united_states=>{:district_of_coloumbia=>{:washington=>nil}}}
hash = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
hash.merge!(countries)
hash[:canada][:ontario][:ottawa] = nil
hash[:canada][:manitoba][:winnipeg] = :me
hash
=> {:canada=>{:ontario=>{:ottawa=>nil}, :manitoba=>{:winnipeg=>:me}}, :united_states=>{:district_of_coloumbia=>{:washington=>nil}}}
Yes, recursion is an option. Here's how it could be implemented.
Code
def get(hash, arr)
case arr.size
when 1 then hash[arr.first]
else get(hash[arr.first], arr[1..-1])
end
end
def set(hash, arr, val)
case arr.size
when 1 then hash[arr.first] = val
else set(hash[arr.first], arr[1..-1], val)
end
end
Example
hash = {
canada: {
ontario:
{ ottawa: :me },
manitoba:
{ winnipeg: nil }
},
united_states: {
district_of_columbia:
{ washington: nil }
}
}
arr_can = [:canada, :ontario, :ottawa]
arr_us = [:united_states, :district_of_columbia, :washington]
get(hash, arr_can) #=> :me
get(hash, arr_us) #=> nil
set(hash, arr_can, 'cat')
set(hash, arr_us, 'dog')
hash
# => {:canada=>{:ontario=> {:ottawa=>"cat"},
# :manitoba=>{:winnipeg=>nil}},
# :united_states=>
# {:district_of_columbia=>{:washington=>"dog"}}
# }
I think recursion is your best option. I wouldn't consider it "overthinking" I'd do:
def getter(hash, array)
return hash[array[0]] if array.count == 1
getter(hash[array[0]], array[1..-1], item)
end
def setter(hash, array, item)
return hash[array[0]] = item if array.count == 1
setter(hash[array[0]], array[1..-1], item)
end

How can I stringify a BSON object inside of a MongoDB map function?

I have documents with field xyz containing
{ term: "puppies", page: { skip: 1, per_page: 20 } } // not useful as a composite key...
{ page: { skip: 1, per_page: 20 }, term: "puppies" } // different order, same contents
For the sake of determining the "top" values in xyz, I want to map them all to something like
emit('term="puppies",page={ skip: 1, per_page: 20 }', 1); // composite key
but I can't get the embedded objects into a meaningful strings:
emit('term="puppies",page=[object bson_object]', 1); // not useful
Any suggestions for a function to use instead of toString()?
# return the top <num> values of <field> based on a query <selector>
#
# example: top(10, :xyz, {}, {})
def top(num, field, selector, opts = {})
m = ::BSON::Code.new <<-EOS
function() {
var keys = [];
for (var key in this.#{field}) {
keys.push(key);
}
keys.sort ();
var sortedKeyValuePairs = [];
for (i in keys) {
var key = keys[i];
var value = this.#{field}[key];
if (value.constructor.name == 'String') {
var stringifiedValue = value;
} else if (value.constructor.name == 'bson_object') {
// this just says "[object bson_object]" which is not useful
var stringifiedValue = value.toString();
} else {
var stringifiedValue = value.toString();
}
sortedKeyValuePairs.push([key, stringifiedValue].join('='));
}
// hopefully we'll end up with something like
// emit("term=puppies,page={skip:1, per_page:20}")
// instead of
// emit("term=puppies,page=[object bson_object]")
emit(sortedKeyValuePairs.join(','), 1);
}
EOS
r = ::BSON::Code.new <<-EOS
function(k, vals) {
var sum=0;
for (var i in vals) sum += vals[i];
return sum;
}
EOS
docs = []
collection.map_reduce(m, r, opts.merge(:query => selector)).find({}, :limit => num, :sort => [['value', ::Mongo::DESCENDING]]).each do |doc|
docs.push doc
end
docs
end
Given that MongoDB uses SpiderMonkey as its internal JS engine, can't you use JSON.stringify (will work even if/when MongoDB switches to V8) or SpiderMonkey's non-standard toSource method?
(sorry, can't try it ATM to confirm it'd work)
toSource method will do the work, but it adds also brackets.
for a clean document use:
value.toSource().substring(1, value.toSource().length - 1)

Resources