I have the following Array (no limit of deepness) :
[
{
name: "foo",
age: 12,
children: [{
name: "zoo",
age: 44
},
{
name: "taz",
age: 17,
children: [{
name: 'tof',
age: 23
},
{
name: 'tok',
age: 42
}
]
}
]
},
{
name: "bar",
age: 54
}
]
And I would like to collect all the different values of the key name, in this example the result would be:
(the order does not matter)
['foo', 'zoo', 'taz', 'tof', 'tok', 'bar']
with a function like
def func(my_array, "name")
Do you have any idea how I should code that function ?
Assuming you know the substructure is under :children:
def extract(sequence, key)
sequence.flat_map do |hash|
[hash[key], *extract(hash[:children] || [], key)]
end
end
Here's a way of doing it using case to differentiate between the various object types you'll encounter:
def find_names(object, key_name)
case (object)
when Array
object.flat_map do |e|
find_names(e, key_name)
end
when Hash
[ object[key_name] ] + find_names(object.values, key_name).compact
end
end
Heres a fast but brittle regex way of doing it
def find_nested(arr, key)
arr.to_s.scan(/#{key.to_sym}=>"([^"]+)/).flatten
end
def find_em(arr)
arr.each_with_object([]) do |h,a|
a << h[:name]
a.concat(find_em(h[:children])) if h.key?(:children)
end
end
find_em(arr)
#=> ["foo", "zoo", "taz", "tof", "tok", "bar"]
Related
I'm having issues understanding how to best manipulate an array to get the data I want. From the research I've done, there's multiple ways, but I'm unclear on which is most optimized.
I want to display a simple list, with the items broken down by country, then state, then organized alphabetically by city. The array is formatted as follows:
[
{
id: 1,
name: "Place 1",
state: "Florida",
city: "Boca Raton",
country: "US",
},
{
id: 2,
name: "Place 2",
state: "Florida",
city: "Daytona Beach",
country: "US",
},
{
id: 3,
name: "Place 3",
state: "Kansas",
city: "Lenexa",
country: "US",
},
{
id: 4,
name: "Place 4",
state: "Harju",
city: "Tallinn",
country: "EE",
},
]
An example of the desired outcome is:
US
Florida
Place 1
Place 2
Kansas
Place 3
EE
Harju
Place 4
I see a lot of people saying to utilize ES6 for this, but I'm not sure the best way to approach it. Manipulate the original array response? Is there some way I can loop through them?
Here's an approach that only requires a single loop.
const data = [];
let result = {};
data.forEach(({ name, state, country }) => {
if (!result[country]) {
result[country] = {};
}
if (!result[country][state]) {
result[country][state] = [name];
}
else {
result[country] = {
...result[country],
[state]: [
...result[country][state],
name
]
};
}
});
console.log(result);
Output
{
US: { Florida: [ 'Place 1', 'Place 2' ], Kansas: [ 'Place 3' ] },
EE: { Harju: [ 'Place 4' ] }
}
I'm sure the if-else part can be removed by using spread operator and operator chaining, but I wasn't able to figure that out.
If your environment supports operator chaining, here's a smaller solution
const data = [];
let result = {};
data.forEach(({ name, state, country }) => {
result[country] = {
...result[country],
[state]: [
...(result?.[country]?.[state] || []),
name
]
};
});
console.log(result);
I have a simple document structure as followed:
{ name: string,
words: [string]
}
I have another string array and would like to generate a sorted list, based on the number of values in the words array that are also in the given array.
for example based on the following data:
[{ name: "jason"
words:["a", "b", "c", "d"]
},
{ name: "thomas"
words:["a", "b", "c", "u"]
}
{ name: "michael"
words:["a", "b", "o", "p",]
}]
The result i wish to achieve based on the input ["a","b","c","d"] would be:
[
{name: "jason", amount: 4},
{name: "thomas", amount: 3}
{name: "michael", amount: 2}
]
The amount key is a nice to have.
Can this be done in a single Mongo-Command?
Removing nils is quite simple, however, I'd like to know:
1) What am I doing wrong, why does my array result below include nils?
2) How I can PREVENT the nils from being added to my array, instead of removing them after the fact.
#cars = Array.new
plucked_array = [
[8, "Chevy", "Camaro", 20],
[9, "Ford", "Mustang", 55],
[9, "Ford", "Fusion", 150]
]
plucked_array.
each { |id, make, model, model_count|
#cars[id] ||= {name: make, id: make, data: []}
#cars[id][:data].push([model, model_count])
}
puts #cars.inspect
#=>[nil, nil, nil, nil, nil, nil, nil, nil, {:name=>"Chevy", :id=>"Chevy", :data=>[["Camaro", 20]]}, {:name=>"Ford", :id=>"Ford", :data=>[["Mustang", 55], ["Fusion", 150]]}]
puts #cars.compact.inspect
#=>[{:name=>"Chevy", :id=>"Chevy", :data=>[["Camaro", 20]]}, {:name=>"Ford", :id=>"Ford", :data=>[["Mustang", 55], ["Fusion", 150]]}]
# This gives the result I'm looking for,
# just wondering how to best get transformed array without the post-cleanup.
I also attempted #theTinMan's recommendation to select first, then map, but I had the same results:
plucked_array.select { |id, make, model, model_count|
#cars[id] = {'name' => make, 'id' => make, 'data' => []}
}.map { |id, make, model, model_count|
#cars[id]['data'].push([model, model_count])
}
puts #cars.inspect
#=>[nil, nil, nil, nil, nil, nil, nil, nil, {:name=>"Chevy", :id=>"Chevy", :data=>[["Camaro", 20]]}, {:name=>"Ford", :id=>"Ford", :data=>[["Mustang", 55], ["Fusion", 150]]}]
I've tried, with partial success, to use Hash instead of an array for #cars. This prevented nils, but my end goal is to build "drilldown: series[]" below, which is an array of hashes:
// Create the chart
Highcharts.chart('container', {
chart: {
type: 'column'
},
title: {
text: 'Imaginary Car Stats'
},
subtitle: {
text: 'Click the columns to view models.'
},
xAxis: {
type: 'category'
},
yAxis: {
title: {
text: 'Total car count'
}
},
legend: {
enabled: false
},
plotOptions: {
series: {
borderWidth: 0,
dataLabels: {
enabled: true,
format: '{point.y}'
}
}
},
tooltip: {
headerFormat: '<span style="font-size:11px">{series.name}</span><br>',
pointFormat: '<span style="color:{point.color}">{point.name}</span>: <b>{point.y:.2f}%</b> of total<br/>'
},
/*I have separate `pluck` query for this top-level series:*/
"series": [
{
"name": "Cars",
"colorByPoint": true,
"data": [
{
"name": "Ford",
"y": 205,
"drilldown": "Ford"
},
{
"name": "Chevy",
"y": 20,
"drilldown": "Chevy"
},
{
"name": "Other",
"y": 16,
"drilldown": null
}
]
}
],
"drilldown": {
/*This is the array of hashes I'm attempting to create:*/
"series": [
{
"name": "Ford",
"id": "Ford",
"data": [
[
"Fusion",
150
],
[
"Mustang",
55
]
]
},
{
"name": "Chevy",
"id": "Chevy",
"data": [
[
"Camaro",
20
]
]
}
]
}
});
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/data.js"></script>
<script src="https://code.highcharts.com/modules/drilldown.js"></script>
<div id="container" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
The reason your code is behaving the way it does is because array[8] = x inserts x at position 8 in the array, and ruby fills the spaces up to 8 with nils.
a = []
a[7] = 4
a == [nil, nil, nil, nil, nil, nil, nil, 4]
You need #cars to be a hash - not an array
I think this will do what you are trying to do:
plucked_array = [
[8, "Chevy", "Camaro", 20],
[9, "Ford", "Mustang", 55],
[9, "Ford", "Fusion", 150]
]
cars = plucked_array.each_with_object({}) do |(id, make, model, count), cars|
cars[id] ||= {id: id, make: make, data: []}
cars[id][:data] << [model, count]
end
p cars.values
Which actually is almost identical to #Austio's solution.
I would go for more of a reduce approach because you are taking a list and boiling it down into another object. each_with_object does the reduce but implicitly returns the obj (in this case cars) in every loop
new_list = plucked_array.each_with_object({}) do |(id, make, model, model_count), cars|
# return before mutating cars hash if the car info is invalid
cars[id] ||= {name: make, id: make, data: []}
cars[id][:data].push([model, model_count])
end
# Then in your controller to handle the usage as an array
#cars = new_list.values
Side note: map is normally more for transforms or equivalent changes, which is i think why it feels off here.
I suggest grouping by id, then creating a hash:
plucked_array.group_by(&:first).transform_values{ |v| v.map{ |id, make, model, model_count| {name: make, id: make, data: [model, model_count]} }}
# {8=>[{:name=>"Chevy", :id=>"Chevy", :data=>["Camaro ls", 20]}],
# 9=>[{:name=>"Ford", :id=>"Ford", :data=>["Mustang", 55]}, {:name=>"Ford", :id=>"Ford", :data=>["Fusion", 150]}]}
# }
Edit - update do match the edit of the original question (get series of data for Highcharts)
This should return the required result:
plucked_array.map.with_object(Hash.new([])) { |(id, make, model, model_count), h|
h[make] += [[model, model_count]] }.map { |k, v| {name: k, id: k, data: v }}
#=> [{:name=>"Chevy", :id=>"Chevy", :data=>[["Camaro", 20]]}, {:name=>"Ford", :id=>"Ford", :data=>[["Mustang", 55], ["Fusion", 150]]}]
The first part builds an hash like this.
#=> {"Chevy"=>[["Camaro", 20]], "Ford"=>[["Mustang", 55], ["Fusion", 150]]}
Passing Hash.new([]) as object allows to insert elements to the default array.
Finally the hash is mapped to the required keys.
Given this sort of data set
records = [
{id: 1, name: "John Smith", age: 49},
{id: 2, name: "Jane Brown", age: 45},
{id: 3, name: "Tim Jones", age: 60},
{id: 4, name: "Blake Owen", age: 78}
]
How do I filter the records to return a reduced array that is just over the age of 50.
Say the return array is
over50s = // something
I've looked at lots of similar code but it's just quite coming together for me.
Thanks for your help!
How about this sample script? The result over50s is retrieved using filter().
Sample script :
records = [
{id: 1, name: "John Smith", age: 49},
{id: 2, name: "Jane Brown", age: 45},
{id: 3, name: "Tim Jones", age: 60},
{id: 4, name: "Blake Owen", age: 78}
]
over50s = records.filter (e) -> e.age >= 50
Result :
[
{ id: 3, name: 'Tim Jones', age: 60 },
{ id: 4, name: 'Blake Owen', age: 78 }
]
If I misunderstand your question, please tell me. I would like to modify.
The answer is perfect, but I wanted to add the "Coffeescript way" using comprehensions:
over50s = (record for record in records when record.age > 50)
Explanation :
for record in records
console.log(record)
# This would loop over the records array,
# and each item in the array will be accessible
# inside the loop as `record`
console.log(record) for record in records
# This is the single line version of the above
console.log(record) for record in records when record.age > 50
# now we would only log those records which are over 50
over50s = (record for record in records when record.age > 50)
# Instead of logging the item, we can return it, and as coffeescript
# implicitly returns from all blocks, including comprehensions, the
# output would be the filtered array
# It's shorthand for:
over50s = for record in records
continue unless record.age > 50
record
# This long hand is better for complex filtering conditions. You can
# just keep on adding `continue if ...` lines for every condition
# Impressively, instead of ending up with `null` or `undefined` values
# in the filtered array, those values which are skipped by `continue`
# are completely removed from the filtered array, so it will be shorter.
I'm trying to place these tools into the right group hash object in this array. I'm not sure how to do this with Ruby.
groups = [
{ group: 'Business Training', tools: [] },
{ group: 'Human Resources', tools: [] },
{ group: 'Clean', tools: [] },
{ group: 'Example', tools: [] }
]
tools = [
{ name: "Foo", group: "Clean", id: 1 },
{ name: "Bar", group: "Clean", id: 2 },
{ name: "Baz", group: "Business Training", id: 3 },
]
I want to end up with a structure like this:
groups = [
{
group: 'Business Training',
tools: [
{ name: "Baz", group: "Business Training", id: 3 },
]
},
{ group: 'Human Resources', tools: [] },
{
group: 'Clean',
tools: [
{ name: "Foo", group: "Clean", id: 1 },
{ name: "Bar", group: "Clean", id: 2 },
]
},
{ group: 'Example', tools: [] }
]
tools.each do |tool|
group = groups.find { |item| item[:group] == tool[:group] }
group[:tools] << tool
end
result = groups.map do |group|
{
group: group[:group],
tools: tools.select do |tool|
tool[:group] == group[:group]
end
}
end
puts result
Which prints:
{:group=>"Business Training", :tools=>[{:name=>"Baz", :group=>"Business Training", :id=>3}]}
{:group=>"Human Resources", :tools=>[]}
{:group=>"Clean", :tools=>[{:name=>"Foo", :group=>"Clean", :id=>1}, {:name=>"Bar", :group=>"Clean", :id=>2}]}
{:group=>"Example", :tools=>[]}
One more solution:
groups.each do |group|
group[:tools] =
tools.select { |tool| tool[:group] == group[:group] }
end
h = tools.each_with_object({}) { |g,h| h[g[:group]] = g }
#=> { "Clean" =>{:name=>"Bar", :group=>"Clean", :id=>2},
# "Business Training"=>{:name=>"Baz", :group=>"Business Training", :id=>3}}
groups.map { |g| g.update(tools: h[g[:group]]) }
#=> [{:group=>"Business Training",
# :tools=>{:name=>"Baz", :group=>"Business Training", :id=>3}},
# {:group=>"Human Resources",
# :tools=>nil},
# {:group=>"Clean",
# :tools=>{:name=>"Bar", :group=>"Clean", :id=>2}},
# {:group=>"Example",
# :tools=>nil}]
Constructing the hash h requires only a single pass through tools. For each element of groups this permits a simple hash value replacement, which is faster than methods that search though tools for each element of groups or vice-versa.
Notice that, as well as mutating groups, this returns the updated value. If you do not wish to modify groups replace Hash#update (aka merge!) with Hash#merge.