Related
I know There is includes Operator on JavaScript Array.
So, Finding common elements from two other Arrays is no Problem. (https://stackoverflow.com/a/1885569/11455650)
Then, What is best way for this on rxjs?
const obs1$ = from(["foo", "bar", "baz", "qux"]);
const obs2$ = from(["bar", "garply", "fred", "foo"];
// const commonIntersection$ = functions or operators...
// result must be ["foo", "bar"]
I think there are two way for implementing this.
Which one is computationally efficient and How can I implement this with rxjs Operator?
merge Two Array and emit only Second value (ignore First Value)
filter each of elements
I assume you want a running intersection from each emission? If so you can either use the scan operator, or roll your own specific intersection operator.
The scan operator is like the reduce method on arrays. In this case, for each element (which is an array of strings) the intersection is returned. Each following emission will work on that last result.
merge(from([["foo", "bar", "baz", "qux"], ["bar", "garply", "fred", "foo"]])).pipe(
map(x => x),
scan((acc, cur) => {
return (!acc)
? next
: acc.filter(x => cur.includes(x));
}, undefined as string[] | undefined),
).subscribe(x => console.log(x));
A custom operator will look cleaner, so if you need it multiple times, then go for it! As you can see the logic below is essentially the same as the scan version. It keeps track of the running intersection state in the acc variable, and the current array is used to update it.
merge(from([["foo", "bar", "baz", "qux"], ["bar", "garply", "fred", "foo"]])).pipe(
map(x => x),
intersection(),
).subscribe(x => console.log(x));
/*...*/
function intersection<T>(): OperatorFunction<T[], T[]> {
return (observable: Observable<T[]>) => {
return new Observable<T[]>((subscriber) => {
let acc: T[] | undefined; // keeps track of the state.
const subscription = observable.subscribe({
next: (cur) => {
acc = (!acc)
? cur
: acc.filter(x => cur.includes(x));
subscriber.next(acc);
},
error: (err) => subscriber.error(err),
complete: () => subscriber.complete(),
});
return () => subscription.unsubscribe();
})
}
}
If you only want the last result then change the scan operator to reduce, or only emit the value of acc in the complete callback of the inner subscription in the operator version.
import { forkJoin, from } from 'rxjs';
import { toArray } from 'rxjs/operators';
import { intersection } from 'lodash';
const obs1$ = from(['foo', 'bar', 'baz', 'qux']);
const obs2$ = from(['bar', 'garply', 'fred', 'foo']);
forkJoin([obs1$.pipe(toArray()), obs2$.pipe(toArray())]).subscribe(
([arr1, arr2]) => console.log(intersection(arr1, arr2))
);
Try here: https://stackblitz.com/edit/rxjs-6jdyzy?file=index.ts
This is not rxjs used codes but used JavaScript Native Array methods (filter, includes).
It seems faster than rxjs operator.
const intersectionArray = combineLatest({
firstArray: [["foo", "bar", "baz", "qux"]],
secondArray: [["bar", "garply", "fred", "foo"]],
}).pipe(
map((x) => x.firstArray.filter((value) => x.secondArray.includes(value)))
);
intersectionArray.subscribe(console.log);
say I have data structure like List< MAP< String, List>>, I only want to keep the List in map's value,
Like, I want to convert following example:
x = [{"key1" => ["list1", "list1"]}, {"key2" => ["list2", "list2"]},
{"key3" => ["list3", "list3"]}]
to:
y = [["list1", "list1"], ["list2", "list2"], ["list3", "list3"]]
Is there any quick way to do this? Thanks
The quickest thing that comes to mind is to leverage flat_map.
x = [ { "key1" => ["list1", "list1"] },
{ "key2" => ["list2", "list2"] },
{ "key3" => ["list3", "list3"] }]
y = x.flat_map(&:values)
=> [["list1", "list1"], ["list2", "list2"], ["list3", "list3"]]
flat_map is an instance method on Enumerable (https://ruby-doc.org/core-2.6.3/Enumerable.html#method-i-flat_map)
values is an instance method on Hash (https://ruby-doc.org/core-2.6.3/Hash.html#method-i-values)
If you only have 1 key in those hashes then you can do it like this:
y = x.map { |h| h.values[0] }
I am working with an array in this form:
"car_documents_attributes"=>{
"1562523330183"=>{
"id"=>"", "filename"=>"tyYYqHeqSFOnqLHEz5lO_rc_tispor12756_6wldwu.pdf", "document_type"=>"contract"
},
"1562523353208"=>{
"id"=>"", "filename"=>"a9P8TyECRiKbI2YdRVZy_rc_tispor12756_bbtzdz.pdf", "document_type"=>"request"
},
"1562523353496"=>{
"id"=>"", "filename"=>"WCM5FHOfSw6yNSUrfPPm_rc_tispor12756_dqu9r2.pdf", "document_type"=>"notes"
},
...
}
I need to find out if in this array is an item where document_type=contract (there can be none, one or multiple ones).
The way I do it is looping through the array item by item, which can be slow if there are tens of items.
Is there a better and faster way to simply check if in the array is an item with document_type = contract?
That's a hash containing more hashes. What you can do is to access to car_documents_attributes, iterate over those hash values and check if any document_type is "contract":
data = {
"car_documents_attributes" => {
"1562523330183" => { "id" => "", "filename" => "tyYYqHeqSFOnqLHEz5lO_rc_tispor12756_6wldwu.pdf", "document_type" => "contract"},
"1562523353208" => { "id" => "", "filename" => "a9P8TyECRiKbI2YdRVZy_rc_tispor12756_bbtzdz.pdf", "document_type" => "request" },
"1562523353496" => { "id" => "", "filename" => "WCM5FHOfSw6yNSUrfPPm_rc_tispor12756_dqu9r2.pdf", "document_type" => "notes" }
}
}
p data['car_documents_attributes'].any? { |_, doc| doc['document_type'] == 'contract' }
# true
Didn't know it was data coming from the params. If so, you do need to permit what's being received or convert the params to an unsafe hash.
Also, you can try using fetch instead [] when trying to get car_documents_attributes, because if that key isn't in data, it'll throw nil, which would throw a NoMethodError:
data.fetch('car_documents_attributes', []).any? { |_, doc| doc['document_type'] == 'contract' }
I have a hash here,
#property_hash = {
:code => '',
:fname => '',
:lname => '',
:basic_sal => '',
:emp_type => '',
}
and an array
line = [02,'Firstname', 'LastName', 5800, 'PL']
I want to map the array into the hash like
#property_hash = {
:code => 02,
:fname => 'Firstname',
:lname => 'LastName',
:basic_sal => 5800,
:emp_type => 'PL',
}
what is the best way to do this ??
Thank you
you can try like this:
#property_hash.each_with_index {|(k, v), index| #property_hash[k] = line[index]}
Not best way but that will work
My solution assumes that line has the same order every time. So I define another array with the field names, merge the corresponding array elements together and convert the result into a hash.
line = [02, 'Firstname', 'LastName', 5800, 'PL']
fields = #property_hash.keys
# => [:code, :fname, :lname, :basic_sal, :emp_type]
key_value_pairs = fields.zip(line)
# => [[:code, 2], [:fname, "Firstname"], [:lname, "LastName"], [:basic_sal, 5800], [:emp_type, "PL"]]
#property_hash = Hash[key_value_pairs]
# => {:code=>2, :fname=>"Firstname", :lname=>"LastName", :basic_sal=>5800, :emp_type=>"PL"}
Memory-wise, it is more efficient to change #property_hash in place rather than setting #property_hash equal to a newly-constructed hash. Here is one way to that:
lc = line.dup
#property_hash.update(#property_hash) { lc.shift }
#=> { :code => 02,
:fname => 'Firstname',
:lname => 'LastName',
:basic_sal => 5800,
:emp_type => 'PL' }
This uses the form of Hash#update (aka merge!) that uses a block to determine the value of keys that are present in both hashes being merged, which here is all of the keys.
Here is one more way it can be done:
[#property_hash.keys, line].transpose.to_h
I realize there are many questions of varying degrees of similarity to this one. I've searched at length (using: [ruby] merge array of hashes on key) for them and I have attempted bits and pieces of each answer to try to solve this on my own. Before coming to StackOverflow, I even shared my question with my colleagues who have been equally stumped. This seems to be a unique question or we're all just staring too closely at it to see an otherwise obvious answer.
Essential Requirements
The solution must work with the Ruby 1.8.7 standard library (no gems). Please feel free to additionally illustrate solutions for other versions of Ruby, but doing so will not automatically make one answer better than another.
The structure of the input data cannot be changed by its provider; the entire data structure is delivered as-is. If the data needs to be temporarily rearranged to provide the most efficient answer, that's perfectly fine as long as the output matches the required sample below. In addition, the solution can make no assumptions about the position of the sorting keys within the Hashes.
The source variable cannot be altered in any way; it is immutable at run-time (this is checked), so the result must be provided to a new variable.
The sample data below is fiction but the problem is real. There are other levels of Arrays-of-Hashes that must also be merged on other keys in the same way; so, the very best answer can be generically applied to arbitrary levels of the data structure.
The best solution will be easy to read, maintain, and apply to arbitrary -- though similar -- data structures. It needn't be a one-liner but if you can meet all the requirements in a single line of Ruby code, kudos to you.
Sample Data
If we think of the Apache Tomcat server.xml file as a Ruby data structure rather than XML, it can provide a very good analog for this problem. Assume further that the default configuration is merged upstream -- before being delivered to you -- with data that you must consolidate before some later operation consumes the resulting data structure. The source data will look very much like this:
source = {
:Server => {
:'attribute.port' => 8005,
:'attribute.shutdown' => 'SHUTDOWN',
:Listener => [
{ :'attribute.className' => 'org.apache.catalina.startup.VersionLoggerListener' },
{ :'attribute.className' => 'org.apache.catalina.core.AprLifecycleListener',
:'attribute.SSLEngine' => 'off'},
{ :'attribute.className' => 'org.apache.catalina.core.JasperListener' },
{ :'attribute.className' => 'org.apache.catalina.core.JreMemoryLeakPreventionListener' },
{ :'attribute.className' => 'org.apache.catalina.core.AprLifecycleListener',
:'attribute.SSLEngine' => 'on'}
],
:Service => [
{ :'attribute.name' => 'Catalina',
:Connector => [
{ :'attribute.port' => 8080,
:'attribute.protocol' => 'HTTP/1.1'},
{ :'attribute.port' => 8009,
:'attribute.protocol' => 'AJP/1.3'}
],
:Engine => {
:'attribute.name' => 'Catalina',
:'attribute.defaultHost' => 'localhost',
:Realm => {
:'attribute.className' => 'org.apache.catalina.realm.LockOutRealm',
:Realm => [
{ :'attribute.className' => 'org.apache.catalina.realm.UserDatabaseRealm',
:'attribute.resourceName' => 'UserDatabase'}
]
},
:Host => [
{ :'attribute.name' => 'localhost',
:'attribute.appBase' => 'webapps',
:Valve => [
{ :'attribute.className' => 'org.apache.catalina.valves.AccessLogValve',
:'attribute.directory' => 'logs'}
]
}
]
}
},
{ :'attribute.name' => 'Catalina',
:Connector => [
{ :'attribute.port' => 8080,
:'attribute.protocol' => 'HTTP/1.1',
:'attribute.secure' => true,
:'attribute.scheme' => 'https',
:'attribute.proxyPort' => 443}
]
},
{ :'attribute.name' => 'JSVCBridge',
:Connector => [
{ :'attribute.port' => 8010,
:'attribute.protocol' => 'HTTP/2'}
]
},
{ :'attribute.name' => 'Catalina',
:Engine => {
:Host => [
{ :'attribute.name' => 'localhost',
:Valve => [
{ :'attribute.className' => 'org.apache.catalina.valves.RemoteIpValve',
:'attribute.internalProxies' => '*',
:'attribute.remoteIpHeader' => 'X-Forwarded-For',
:'attribute.protocolHeader' => 'X-Forwarded-Proto',
:'attribute.protocolHeaderHttpsValue' => 'https'}
]
}
]
}
}
]
}
}
The challenge is to produce this result from it:
result = {
:Server => {
:'attribute.port' => 8005,
:'attribute.shutdown' => 'SHUTDOWN',
:Listener => [
{ :'attribute.className' => 'org.apache.catalina.startup.VersionLoggerListener' },
{ :'attribute.className' => 'org.apache.catalina.core.AprLifecycleListener',
:'attribute.SSLEngine' => 'on'},
{ :'attribute.className' => 'org.apache.catalina.core.JasperListener' },
{ :'attribute.className' => 'org.apache.catalina.core.JreMemoryLeakPreventionListener' },
],
:Service => [
{ :'attribute.name' => 'Catalina',
:Connector => [
{ :'attribute.port' => 8080,
:'attribute.protocol' => 'HTTP/1.1',
:'attribute.secure' => true,
:'attribute.scheme' => 'https',
:'attribute.proxyPort' => 443},
{ :'attribute.port' => 8009,
:'attribute.protocol' => 'AJP/1.3'}
],
:Engine => {
:'attribute.name' => 'Catalina',
:'attribute.defaultHost' => 'localhost',
:Realm => {
:'attribute.className' => 'org.apache.catalina.realm.LockOutRealm',
:Realm => [
{ :'attribute.className' => 'org.apache.catalina.realm.UserDatabaseRealm',
:'attribute.resourceName' => 'UserDatabase'}
]
},
:Host => [
{ :'attribute.name' => 'localhost',
:'attribute.appBase' => 'webapps',
:Valve => [
{ :'attribute.className' => 'org.apache.catalina.valves.AccessLogValve',
:'attribute.directory' => 'logs'},
{ :'attribute.className' => 'org.apache.catalina.valves.RemoteIpValve',
:'attribute.internalProxies' => '*',
:'attribute.remoteIpHeader' => 'X-Forwarded-For',
:'attribute.protocolHeader' => 'X-Forwarded-Proto',
:'attribute.protocolHeaderHttpsValue' => 'https'}
]
}
]
}
},
{ :'attribute.name' => 'JSVCBridge',
:Connector => [
{ :'attribute.port' => 8010,
:'attribute.protocol' => 'HTTP/2'}
]
}
]
}
}
The Question
We need source to become result. To get there, :Listener gets merged by attribute.className; :Service gets merged by attribute.name; the resulting Arrays of :Connector get merged by attribute.port; and such. The identification of the location of the Arrays-of-Hashes within the data structure and the key which each is to be merged on should be easily provided to the solution.
The real essence of this question is finding that generic solution that can apply to multiple arbitrary levels of a complex data structure like this, merge Arrays-of-Hashes by a supplied key, and produce the merged result after the set of location and key pairs is provided.
Thank you all very much for your time and interest in this question.
There may be more elegant ways of condensing this code but I finally developed an answer to this very challenging question. While Wand Maker's answer came close, it was based on the untenable assumption that the order of the keys in the Hashes would be predictable and stable. As this is a Ruby 1.8.7 problem and because the data provider makes no such guarantee, I had to take a different path; we had to inform the merge engine which key to use for each Array-of-Hashes.
My (non-optimized) solution required three functions and an external Hash that defines the necessary merge keys:
deepMergeHash walks through a hash, deeply scanning for Arrays
deepMergeArrayOfHashes performs the desired merge against an Array-of-Hashes
subMergeHelper recursively assists deepMergeArrayOfHashes
The trick was to not only treat the Hash recursively, but to always be aware of the "present" location within the Hash so that the necessary merge key could be known. Having established a way to determine that location, defining, finding, and using the merge keys became trivial.
The Solution
def subMergeHelper(lhs, rhs, mergeKeys, crumbTrail)
lhs.merge(rhs){|subKey, subLHS, subRHS|
mergeTrail = crumbTrail + ':' + subKey.to_s
case subLHS
when Array
deepMergeArrayOfHashes(subLHS + subRHS, mergeKeys, mergeTrail)
when Hash
subMergeHelper(subLHS, subRHS, mergeKeys, mergeTrail)
else
subRHS
end
}
end
def deepMergeArrayOfHashes(arrayOfHashes, mergeKeys, crumbTrail)
mergedArray = arrayOfHashes
if arrayOfHashes.all? {|e| e.class == Hash}
if mergeKeys.has_key?(crumbTrail)
mergeKey = mergeKeys[crumbTrail]
mergedArray = arrayOfHashes.group_by{|evalHash| evalHash[mergeKey.to_sym]}.map{|groupID, groupArrayOfHashes|
groupArrayOfHashes.reduce({}){|memoHash, evalHash|
memoHash.merge(evalHash){|hashKey, lhs, rhs|
deepTrail = crumbTrail + ':' + hashKey.to_s
case lhs
when Array
deepMergeArrayOfHashes(lhs + rhs, mergeKeys, deepTrail)
when Hash
subMergeHelper(lhs, rhs, mergeKeys, deepTrail)
else
rhs
end
}
}
}
else
$stderr.puts "[WARNING] deepMergeArrayOfHashes: received an Array of Hashes without merge key at #{crumbTrail}."
end
else
$stderr.puts "[WARNING] deepMergeArrayOfHashes: received an Array containing non-Hashes at #{crumbTrail}?"
end
return mergedArray
end
def deepMergeHash(hashConfig, mergeKeys, crumbTrail = '')
return hashConfig unless Hash == hashConfig.class
mergedConfig = {}
hashConfig.each{|nodeKey, nodeValue|
nodeCrumb = nodeKey.to_s
testTrail = crumbTrail + ':' + nodeCrumb
case nodeValue
when Hash
mergedConfig[nodeKey] = deepMergeHash(nodeValue, mergeKeys, testTrail)
when Array
mergedConfig[nodeKey] = deepMergeArrayOfHashes(nodeValue, mergeKeys, testTrail)
else
mergedConfig[nodeKey] = nodeValue
end
}
return mergedConfig
end
Example Use
Using the data in the question, we can now:
mergeKeys = {
':Server:Listener' => 'attribute.className',
':Server:Service' => 'attribute.name',
':Server:Service:Connector' => 'attribute.port',
':Server:Service:Engine:Host' => 'attribute.name',
':Server:Service:Engine:Host:Valve' => 'attribute.className',
':Server:Service:Engine:Realm:Realm' => 'attribute.className'
}
mergedConfig = deepMergeHash(source, mergeKeys)
I can't seem to perform a successful equality test like (result == mergedConfig), but a visual inspection of mergedConfig shows that it is identical to result except that the order of some keys changes. I suspect that's a side-effect of using Ruby 1.8.x and is acceptable for this question.
Happy coding, everyone and thank you so much for your interest in this discussion.
Solution based on assumption that you are merging hashes based on value of first key in the given array of hashes is given below:
def merge_ary(ary_hash)
# Lets not process something that is not array of hash
return ary_hash if not ary_hash.all? {|h| h.class == Hash }
# If array of hash, lets group them by value of first key
# Then, reduce the resultant group of hashes by merging them.
c = ary_hash.group_by {|h| h.values.first}.map do |k,v|
v_reduced = v.reduce({}) do |memo_hash, h|
memo_hash.merge(h) do |k, v1, v2|
v1.class == Array ? merge_ary(v1 + v2) : v2
end
end
[k, v_reduced]
end
return Hash[c].values
end
def merge_hash(hash)
t = hash.map do |k,v|
new_v = v
if v.class == Hash
new_v = merge_hash(v)
elsif v.class == Array
new_v = merge_ary(v)
end
[k,new_v]
end
return Hash[t]
end
# Test the output
merge_hash(source) == result
#=> true