Clojure: into {} doesn't preserve sort order - sorting

I have a nested map which is structured like this (Clojurescript):
{"6841"
{"primaryTitle" "First name",
"secondaryTitle" "last name"},
"7944"
{"primaryTitle" "Test 2 first name",
"secondaryTitle" "Test 2 last name"}}
I then proceed to sort the map with the keys inside the nested map, like this:
(defn compare-title [x y]
(compare [(get (second x) "primaryTitle") (get (second x) "secondaryTitle")]
[(get (second y) "primaryTitle") (get (second y) "secondaryTitle")]))
(sort compare-title #loaded-assets)
So far the sorting works fine, but since sort function return the data structure like this:
["6841"
{"primaryTitle" "First name",
"secondaryTitle" "last name"}],
["7944"
{"primaryTitle" "Test 2 first name",
"secondaryTitle" "Test 2 last name"}]}
I have to use into {} to transform the map back to the initial structure:
(into {} (sort compare-title my-map))
But this completely reverses the sorting which is made by sort. I've tried to replace into {} with:
flatten (which transforms this into list)
apply hash-map (which behaves similar to into {})
reduce hash-map (which preserves the order, but deeply nests each map into each other)
So, is it possible to sort the map while preserving the structure?
Or how to transform back to the above original structure while preserving the sorted structure returned by sort?

You can use priority-map
(use 'clojure.data.priority-map)
(defn compare-title [x y]
(compare [(get x "primaryTitle") (get x "secondaryTitle")]
[(get y "primaryTitle") (get y "secondaryTitle")]))
(apply priority-map-by compare-title
["7944"
{"primaryTitle" "Test 2 first name"
"secondaryTitle" "Test 2 last name"}
"6841"
{"primaryTitle" "First name"
"secondaryTitle" "last name"}])
;; => {"6841" {"primaryTitle" "First name", "secondaryTitle" "last name"}, "7944" {"primaryTitle" "Test 2 first name", "secondaryTitle" "Test 2 last name"}}

As already mentioned, maps cannot be sorted by values. They can be sorted by keys. Here is the easiest way with some helper functions:
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test))
(def data
{"6841"
{"primaryTitle" "First name",
"secondaryTitle" "last name"},
"7944"
{"primaryTitle" "Test 2 first name",
"secondaryTitle" "Test 2 last name"}})
(def data-sorted ; maybe use `postwalk` instead
(->sorted-map
(map-vals data ->sorted-map)))
(dotest
(is= data-sorted
{"6841" {"primaryTitle" "First name",
"secondaryTitle" "last name"},
"7944" {"primaryTitle" "Test 2 first name",
"secondaryTitle" "Test 2 last name"}}))
If you want to sort by the primary/secondary title, put those 2 items in a "sort key", then sort based on that:
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test))
(def all-data {"7944" {"primaryTitle" "Test 2 first name"
"secondaryTitle" "Test 2 last name"}
"6841" {"primaryTitle" "First name"
"secondaryTitle" "last name"}})
(dotest
(let [data-keyed (forv [entry all-data]
(let [[id title-map] entry]
{:sort-key [(grab "primaryTitle" title-map)
(grab "secondaryTitle" title-map)]
:id id}))
data-sorted (vec (sort-by :sort-key data-keyed))]
(is= (spy-pretty data-keyed)
[{:sort-key ["Test 2 first name" "Test 2 last name"] :id "7944"}
{:sort-key ["First name" "last name"] :id "6841"}])
(is= (spy-pretty data-sorted)
[{:sort-key ["First name" "last name"] :id "6841"}
{:sort-key ["Test 2 first name" "Test 2 last name"] :id "7944"}])))

Related

How to convert from Array to Hashmap

I have a string:
{:id 1, :name "Ab Bc Cd", :sex "M", :birth "01.01.1999", :address "Street 1", :oms "0001"}
And i need to convert it to HashMap but
(hash-map my-str)
returns {}
So i splited it:
(s/split my-str ",")
And it returns
[:id 1 :name "Ab Bc Cd" :sex "M" :birth "01.01.1999" :address "Street 1" :oms "0001"]
Then
for [x my-arr]
(hash-map x)
returns
({} {} {} {} {} {})
How can i do this?
If you got this string intentionally, then use
clojure.edn/read-string
E.g.
user=> (require 'clojure.edn)
nil
user=> (clojure.edn/read-string (slurp "x.edn"))
{:id 1, :name "Ab Bc Cd", :sex "M", :birth "01.01.1999", :address "Street 1", :oms "0001"}
your example is almost there!
you can finish it with a call to
(into {} your-result-so-far)
into comes up a lot of you get into the habit of looking for it.
The shortest way would be clojure.core's read-string - it reads from the string and executes the command for building an object:
(def s "{:id 1, :name \"Ab Bc Cd\", :sex \"M\", :birth \"01.01.1999\", :address \"Street 1\", :oms \"0001\"}")
(read-string s)
;; => {:id 1, :name "Ab Bc Cd", :sex "M", :birth "01.01.1999", :address "Street 1", :oms "0001"}
The documentation says:
Reads one object from the string s. Optionally
include reader options, as specified in read. Note that read-string
can execute code (controlled by read-eval), and as such should be
used only with trusted sources. For data structure interop use
clojure.edn/read-string

Clean way to push results of multiple methods into an array

I have the following Ruby code, which calls the same function multiple times with different arguments, and pushes the results into a common array.
people_relations = []
people.zip(people_addresses).map do |person, address|
people_relations.push(createRelation(person, address))
end
people.zip(people_ph_numbers).map do |person, phone_number|
people_relations.push(createRelation(person, phone_number))
end
people.zip(people_aliases).map do |person, _alias|
people_relations.push(createRelation(person, _alias))
end
def createRelation(e1, e2)
[true, false].sample ? CurrentRelation.new(e1, e2) : PastRelation.new(e1, e2)
end
This code works just fine, but I feel like this is not the idiomatic Ruby way of doing things, and can be improved by compressing the code into less lines or made to look cleaner.
Is there a better way to write the code that appears above?
You could create an array that contains all the people "attributes" you're going to use, and with Enumerable#each_with_object you can assign an initial array to fill with the result of each call to createRelation():
attributes = [people_addresses, people_ph_numbers, people_aliases]
relations = people.each_with_object([]).with_index do |(person, memo), index|
attributes.each do |attribute|
memo << createRelation(person, attribute[index])
end
end
I'd probably go with a transpose -> flat_map solution for this myself, for instance given:
def CreateRelation(person, relationship)
if [true, false].sample
"#{person} is currently related to #{relationship}"
else
"#{person} used to be related to #{relationship}"
end
end
people = ['Person 1', 'Person 2', 'Person 3']
addresses = ['Person 1 Address', 'Person 2 Address', 'Person 3 Address']
phone_numbers = ['Person 1 Phone', 'Person 2 Phone', 'Person 3 Phone']
aliases = ['Person 1 AKA', 'Person 2 AKA', 'Person 3 AKA']
We can stick those 4 arrays into a single array and then transpose them, so the first element of each ends up in an array with each other, the second in another, and the last in a third:
[people, addresses, phone_numbers, aliases].transpose # => [
# ["Person 1", "Person 1 Address", "Person 1 Phone", "Person 1 AKA"],
# ["Person 2", "Person 2 Address", "Person 2 Phone", "Person 2 AKA"],
# ["Person 3", "Person 3 Address", "Person 3 Phone", "Person 3 AKA"]]
and then you can flat_map those by calling CreateRelation:
result = [people, addresses, phone_numbers, aliases].transpose.flat_map do |person, *relations|
relations.map { |relationship| CreateRelation(person, relationship) }
end
#["Person 1 used to be related to Person 1 Address",
# "Person 1 used to be related to Person 1 Phone",
# "Person 1 used to be related to Person 1 AKA",
# "Person 2 is currently related to Person 2 Address",
# "Person 2 used to be related to Person 2 Phone",
# "Person 2 is currently related to Person 2 AKA",
# "Person 3 is currently related to Person 3 Address",
# "Person 3 used to be related to Person 3 Phone",
# "Person 3 used to be related to Person 3 AKA"]
Or, at that point you could stick with just iterating and pushing, if you don't want to map/flat_map.
The more I think about it, the more I think I'd go with transpose -> each_with_object, instead of flat_map...less "create an array and then throw it away", I'll leave this with flat_map though because it is another option and #Sebastian Palma has each_with_object covered.

How to iterate nested PersistentArrayMap in Clojure/Script

After executing a query against the db, the return of the fuction is the list of maps:
({:id 1 :name "Book 1" :category "Drama"}
{:id 2 :name "Book 2" :category "Drama"}
{:id 3 :name "Book 3" :category "Poetry"}
{:id 4 :name "Book 4" :category "Poetry"}
{:id 5 :name "Book 5" :category "Fantasy"}
{:id 6 :name "Book 6" :category "Fantasy"}
{:id 7 :name "Book 7" :category "Fantasy"}
{:id 8 :name "Book 8" :category "Science fiction"}
{:id 9 :name "Book 9" :category "Science fiction"}
{:id 10 :name "Book 10" :category "Science fiction"}
...)
So, I group data by category and group-by function returns a persistent array-map contains strs keys and vector of maps as vals:
{"Fantasy" [{:category "Fantasy", :name "Book 5", :id 5}
{:category "Fantasy", :name "Book 6", :id 6}
{:category "Fantasy", :name "Book 7", :id 7}],
"Drama" [{:category "Drama", :name "Book 1", :id 1}
{:category "Drama", :name "Book 2", :id 2}],
"Poetry" [{:category "Poetry", :name "Book 3", :id 3}
{:category "Poetry", :name "Book 4", :id 4}],
"Science fiction" [{:category "Science fiction",
:name "Book 8",
:id 8}
{:category "Science fiction",
:name "Book 9",
:id 9}
{:category "Science fiction",
:name "Book 10",
:id 10}]}
Next, I do this:
(doseq [[k [{:keys [id name]} v]] data]
(println k)
(println id name))
The side-effect is:
Drama
1 Book1
Poetry
3 Book3
Fantasy
5 Book5
Science fiction
8 Book8
doseq returned only one value for each key.
How can I get the rest of the values?
The result must be:
Drama
1 Book1
2 Book2
Poetry
3 Book3
4 Book4
Fantasy
5 Book5
6 Book6
7 Book7
Science fiction
8 Book8
9 Book9
10 Book10
you can just make an inner loop like this:
(doseq [[k vs] data]
(println k)
(doseq [{:keys [id name]} vs]
(println id name)))
or using single doseq:
(doseq [[k vs] data
{:keys [id name] :as v} vs]
(when (= v (first vs))
(println k))
(println id name))
also there is one more dirty way to print outer loop string once:
(doseq [[k vs] data
:when (or (println k) true)
{:keys [id name] :as v} vs]
(println id name))
or even like this:
(doseq [[k vs] data
{:keys [id name] :as v} (do (println k) vs)]
(println id name))

Pulling out Keys and Values from an hash of arrays

I have an hash like this -
{"examples"=>
[{"year"=>1999,
"provider"=>{"name"=>"abc", "id"=>711},
"url"=> "http://example.com/1",
"reference"=>"abc",
"text"=> "Sample text 1",
"title"=> "Sample Title 1",
"documentId"=>30091286,
"exampleId"=>786652043,
"rating"=>357.08115},
{"year"=>1999,
"provider"=>{"name"=>"abc", "id"=>3243},
"url"=> "http://example.com/2",
"reference"=>"dec",
"text"=> "Sample text 2",
"title"=> "Sample Title 2",
"documentId"=>30091286,
"exampleId"=>786652043,
"rating"=>357.08115},
{"year"=>1999,
"provider"=>{"name"=>"abc", "id"=>191920},
"url"=> "http://example.com/3",
"reference"=>"wer",
"text"=> "Sample text 3",
"title"=> "Sample Title 3",
"documentId"=>30091286,
"exampleId"=>786652043,
"rating"=>357.08115}]
}
and I would like to create a new array by pulling out the keys, and values for just the "text", "url" and "title" keys like below.
[
{"text"=> "Sample text 1", "title"=> "Sample Title 1", "url"=> "http://example.com/1"},
{"text"=> "Sample text 2", "title"=> "Sample Title 2", "url"=> "http://example.com/2"},
{"text"=> "Sample text 3", "title"=> "Sample Title 3", "url"=> "http://example.com/3"}
]
Any help is sincerely appreciated.
You should do as
hash['examples'].map do |hash|
keys = ["text", "title", "url"]
keys.zip(hash.values_at(*keys)).to_h
end
If you are below < 2.1 use,
Hash[keys.zip(hash.values_at(*keys))]
Here's another way this could be done (where h is the hash given in the question).
KEEPERS = ['text','url','title']
h.each_key.with_object({}) { |k,g|
g[k] = h[k].map { |h| h.select { |sk,_| KEEPERS.include? sk } } }
#=> {"examples"=>[
# [{"url"=>"http://example.com/1", "text"=>"Sample text 1",
# "title"=>"Sample Title 1"},
# {"url"=>"http://example.com/2", "text"=>"Sample text 2",
# "title"=>"Sample Title 2"},
# {"url"=>"http://example.com/3", "text"=>"Sample text 3",
# "title"=>"Sample Title 3"}]}
Here we simply create a new hash (denoted by the outer block variable g) which has all the keys of the original hash h (just one, "examples", but there could be more), and for each associated value, which is an array of hashes, we use Enumerable#map and Hash#select to retain only the desired key/value pairs from each of those hashes.

Ruby array trouble

I am trying to learn Ruby, and arrays are giving me some trouble.
I have input that I flatten down to the pattern "name, number, name, number". I then want to make an array of 2-element arrays, each containing a name and the next number.
When I push these 2-element arrays into another array the seem to automatically flatten to a 0-dimensional array. What I want is the final array to be of size [N/2][2], N being number of names, or numbers in the input.
http://pastie.org/3542269
The puts with the comment does not happen until all of the elements from the pairs array has been printed, so it looks like this:
Name
1
Name
2
Name
3
When I expected this:
Name
1
Name
2
Name
3
I guess my questions are:
How do I put arrays inside an array, to make a jagged one?
How do I keep track of how many dimensions my arrays are in Ruby? It's so much easier when you have to declare a size.
some_array = [[["Name 1", "value 1"], ["Name 2", "value 2"]], [["Name 3", "value 3"], ["Name 4", "value 4"]]]
array = some_array.flatten
new_array = array.each_slice(2).map do |a, b|
[a,b]
end
#=> [["Name 1", "value 1"],
#=> ["Name 2", "value 2"],
#=> ["Name 3", "value 3"],
#=> ["Name 4", "value 4"]]
which is similar to some_array.flatten(1)

Resources