Ruby array, convert to two arrays in my case - ruby-on-rails

I have an array of string which contains the "firstname.lastname?some.xx" format strings:
customers = ["aaa.bbb?q21.dd", "ccc.ddd?ew3.yt", "www.uuu?nbg.xcv", ...]
Now, I would like to use this array to produce two arrays, with:
the element of the 1st array has only the string before "?" and replace the "." to a space.
the element of the 2nd array is the string after "?" and include "?"
That's I want to produce the following two arrays from the customers array:
1st_arr = ["aaa bbb", "ccc ddd", "www uuu", ...]
2nd_arr = ["?q21.dd", "?ew3.yt", "?nbg.xcv", ...]
What is the most efficient way to do it if I use customers array as an argument of a method?
def produce_two_arr customers
#What is the most efficient way to produce the two arrays
#What I did:
1st_arr = Array.new
2nd_arr = Array.new
customers.each do |el|
1st_Str, 2nd_Str=el.split('?')
1st_arr << 1st_str.gsub(/\./, " ")
2nd_arr << "?"+2nd_str
end
p 1st_arr
p 2nd_arr
end

Functional approach: when you are generating results inside a loop but you want them to be split in different arrays, Array#transpose comes handy:
ary1, ary2 = customers.map do |customer|
a, b = customer.split("?", 2)
[a.gsub(".", " "), "?" + b]
end.transpose

Anytime you're building an array from another, reduce (a.k.a. inject) is a great help:
But sometimes, a good ol' map is all you need (in this case, either one works because you're building an array of the same size):
a, b = customers.map do |customer|
a, b = customer.split('?')
[a.tr('.', ' '), "?#{b}"]
end.transpose
This is very efficient since you're only iterating through customers a single time and you are making efficient use of memory by not creating lots of extraneous strings and arrays through the + method.

Array#collect is good for this type of thing:
arr1 = customers.collect{ |c| c.split("?").first.sub( ".", "" ) }
arr2 = customers.collect{ |c| "?" + c.split("?").last }
But, you have to do the initial c.split("?") twice. So, it's effecient from an amount of code point of view, but more CPU intensive.

1st_arr = customers.collect{ |name| name.gsub(/\?.*\z/,'').gsub(/\./,' ') }
2nd_arr = customers.collect{ |name| name.match(/\?.*\z/)[0] }

array1, array2 = customers.map{|el| el.sub('.', ' ').split /(?:\?)/}.transpose
Based on #Tokland 's code, but it avoids the extra variables (by using 'sub' instead of 'gsub') and the re-attaching of '?' (by using a non-capturing regex).

Related

Why does ruby sort Alphanumeric strings with 1's and 0's as binary?

I have an array ["Q10", "Q100", "Q1000", "Q1000a", "Q1001", "Q98"]. After sorting it, I get the following result:
['Q100', 'Q1000', 'Q1000a','Q98', 'Q10', 'Q1001'].sort
["Q10", "Q100", "Q1000", "Q1000a", "Q1001", "Q98"]
Because of this behaviour, I cannot sort my ActiveRecord objects correctly. I have a model of Question which has a field label. I need to sort it based on label. So Question with label Q1 would be first and the question with label Q1a would follow and so on. I get in a similar order with ActiveRecord described to the above example of array. I am using postgresql as my database.
Now I have 3 questions.
Why alphanumeric string sorting behave that way?
How can I achieve my required sorting without using the sort block?
How can I achieve that sorting in ActiveRecord?
If your array were
arr = ["Q10", "Q100", "Q1000", "Q8", "Q1001", "Q98"]
you could write
arr.sort_by { |s| s[/\d+/].to_i }
#=> ["Q8", "Q10", "Q98", "Q100", "Q1000", "Q1001"]
If
s = "Q1000"
then
s[/\d+/].to_i
#=> 1000
See Enumerable#sort_by and String#[].
The regular expression /\d+/ matches a substring of s that contains one or more digits.
If the array were
arr = ["Q10b", "Q100", "Q1000", "Q10a", "Q1001", "Q98", "Q10c"]
you could write
arr.sort_by { |s| [s[/\d+/].to_i, s[/\D+\z/]] }
#=> ["Q10a", "Q10b", "Q10c", "Q98", "Q100", "Q1000", "Q1001"]
If
s = "Q10b"
then
[s[/\d+/].to_i, s[/\D+\z/]]
#=> [10, "b"]
The regular expression /\D+\z/ matches a substring of s that contains one or more non-digits at the end (\z) of the string.
See Array#<=>, specifically the third paragraph, for an explanation of how arrays are ordered when sorting.
If the array were
arr = ["Q10b", "P100", "Q1000", "PQ10a", "Q1001", "Q98", "Q10c"]
you could write
arr.sort_by { |s| [s[/\A\D+/], s[/\d+/].to_i, s[/\D+\z/]] }
#=> ["P100", "PQ10a", "Q10b", "Q10c", "Q98", "Q1000", "Q1001"]
If
s = "PQ10a"
then
[s[/\A\D+/], s[/\d+/].to_i, s[/\D+\z/]]
#=> ["PQ", 10, "a"]
The regular expression /\A\D+/ matches a substring of s that contains one or more non-digits at the beginning (\A) of the string.
This should do the trick for you, casting them to numbers before sorting.
['100', '1000', '98', '10', '1001'].map(&:to_i).sort
This strange map(&:to_i) is shorthand for map { |x| x.to_i }
Edit:
You could do this with AR. This will throw an error if the column doesn't contain a number disguised as a string.
Model.order("some_column::integer")
Edit II:
Try this if it contains strings as well.
Model.order("cast(some_column as integer))"

How to compute each cell of a line in an array with Ruby on Rails 5.2?

While importing from an excel file to a database, I need to format a hierarchy so it appears with leading zeros:
10.1.1.4 must be transformed into 1.010.001.001.004
I tried to iterate through and concatenate the elements:
record.hierarchy = spreadsheet.cell(i,2).split('.').each do |t|
index = index || '1.'
index = index + '.' + (((t.to_i + 1000).to_s).last(3))
end
which actually returns and array of ["10", "1", "1", "4"], not computed. I would expect this to return the last evaluated value: index
I tried to compute it directly inside the array:
record.hierarchy = '1.' + (((spreadsheet.cell(i,2).split('.').each).to_i + 1000).to_s).last(3).join('.')
which raises an undefined method to_i for enumerator.
Can someone explain me how to structure and solve this computation?
Thanks
Use #rjust.
'10.1.1.4'.split('.').map { |l| l.rjust(3, '0') }.join('.')
Your first solution uses assignment with #each. #each will not return modified array.
It is not necessary to convert the string to an array, modify the elements of the array and then join the array back into a string. The string can be modified directly using String#gsub.
str = '10.1.1.4'
('1.' + str).gsub(/(?<=\.)\d+/) { |s| sprintf("%03d", s.to_i) }
#=> "1.010.001.001.004"
See Kernel#sprintf.
(?<=\.) is positive lookbehind that requires the matched digits to be preceded by a period. I've assumed the string is known to contain between one and three digits before and after each period.
You can try different function for leading zeroes and inject to not set default value inside the loop
record.hierarchy = spreadsheet.cell(i,2).split('.').inject('1') do |result, t|
result + '.' + t.rjust(3, '0')
end

Check if multiple (more than two) arrays match, regardless of element order in ruby

If I have two arrays a and b. I can compare them as a.uniq.sort == b.uniq.sort. This will tell me if the elements in a match those in b.
What if I have four arrays a, b, c, d? I need to make sure they are all equal to each other. What is the best way to do this?
[%w[a b c], %w[c b a], %w[a a b c], %w[b a b c]]
.group_by{|a| a.uniq.sort}.one? # => true
[%w[a b c], %w[c b a], %w[a a b c], %w[b a b c d]]
.group_by{|a| a.uniq.sort}.one? # => false
Take one array, sort it's unique elements and compare the other arrays with that as reference. Return false as soon as one is not equal.
ars = [a,b,c,d]
ref = ars.pop.uniq.sort #take care: pop mutates ars
p ars.all?{|ar| ar.uniq.sort == ref}
Here's a way that doesn't involve sorting.
Let arr be an array of the given arrays. If the arrays contained no duplicates, this would be simple:
first, *rest = arr
rest.all? { |a| (first-a).empty? && (a-first).empty? }
To deal with duplicates, we define a helper Array#difference:
class Array
def difference(other)
h = other.each_with_object(Hash.new(0)) { |e,h| h[e] += 1 }
reject { |e| h[e] > 0 && h[e] -= 1 }
end
end
In a nutshell, if n elements of an array a equal a given object and m elements of an array b equal the same object, the array a.difference(b) will contain (the first) [n-m,0].max elements of a that equal the object .
Then we can write the following.
first, *rest = arr
rest.all? { |a| first.difference(a).empty? && a.difference(first).empty? }
The block evaluates true if every element of a maps to a unique element of first, and vice-versa.
It may seem a waste of time to create Array#difference for just this one problem. I have found, however, it to be a valuable member of my tool kit, having wide application, so much so that I proposed it be added to the Ruby core. The link gives examples of its uses and also contains a link to an SO answer I gave that contains a longer list of problems where it has found application.
There's a nice reduce solution:
ars.reduce { |a, v| a.uniq.sort == v.uniq.sort ? v.uniq.sort : break }
Which ends up to pretty clean code used in conjunction with map:
ars.map { |v| v.uniq.sort }.reduce { |a, v| a == v ? v : break }
The map -> reduce version is not the the best for performance since all the array elements get uniq.sort'd, but it's readable and functional style.
Since you use uniq you can use intersection, without the uniq intersection couldn't be used to compare arrays.
arr.inject(:&).sort == arr.first.uniq.sort
This would mean that arrays like
[3,2,3,1]
[1,3,2]
[3,2,1]
[2,1,3,1]
would be the same
You can try:
a.uniq.sort == b.uniq.sort and b.uniq.sort == c.uniq.sort and c.uniq.sort == d.uniq.sort
To optimise it, you can store the result of b.uniq.sort and c.uniq.sort in some variable to prevent repeated computation before comparing.

I need explanation of method inject

I am trying to figure out how inject method works
Can someone explain this
def mysort
if djeca.any?
djeca.order(name: :asc).inject([]) { |sum, c| sum += c.mysort}.uniq
else
[self]
end
mysort is method of model class Books
In controller I call method mysort :
#mybooks= Books.find_by(name: 'renesansa')
#mybookss= #mybooks.leaf_wms
djeca.order(name: :asc).inject([]) { |sum, c| sum += c.mysort}.uniq
is equivalent to
sum = []
djeca.order(name: :asc).each{|c| sum = sum + c.mysort}
sum.uniq
Adding arrays is actually concatening, so your code just appends all the c.mysort into an array.
If I understand it correctly, you could also write :
djeca.order(name: :asc).map{|c| c.mysort}.flatten.uniq
Depending on your audience, you might want to write one or the other.
Note that you don't need to assign a value to sum in the inject block, it is done automatically.
(1..10).inject(0){|mem,i| mem += i}
#=> 55
(1..10).inject(0){|mem,i| mem + i}
#=> 55
You should follow this link https://samurails.com/tips/ruby-ruby-on-rails-inject/
For example:
result = [1,2,3,4].inject(0) { |sum, number| sum + number }
Here, the process starts from 0 index to the 3 index. First inject adds 1 & 2 to get the sum of two values and stored in sum variable(i.e. sum= 3) and then it takes the sum value and adds 3 to it, to get result(i.e sum=6) and so on. Finally you will get the result like 10.
djeca.order(name: :asc)
Retrieves array of active records
.inject([])
Looping through each active record. The initial output of inject is empty array [].
{ |sum, c| }
c - each active record
sum - Inject output value. Initially its []. On each iteration the value is appended to the array. (sum += [some values])

Is there a way to apply multiple method by one liner in ruby?

There is an array like this:
a = [1,2,3,4]
I want to get the return values of size and sum like this.
size = a.size
sum = a.sum
Is there a way to get both values by a one-liner like this?
size, sum = a.some_method(&:size, &:sum)
In Ruby, you can do multiple assignments in one line:
size, sum = a.size, a.sum
It doesn't make it more readable, though.
You could do this:
a = [1,2,3,4]
methods = [:size, :max, :min, :first, :last]
methods.map { |m| a.send m }
#=> [4, 4, 1, 1, 4]
Another possible solution:
size, sum = a.size, a.reduce { |a,b| a = a + b }
Previous answers are correct, but if OP was actually concerned about walking the array multiple times, then array.size does not walk the array, it merely returns the length, thus there is no saving from a oneliner in that regard.
On the other hand, if size was just an example and the question is more about making multiple operations on an array in one go, then try something like this:
arr = [1,2,3,4,5,6]
product,sum = arr.inject([1,0]){|sums,el| [sums[0]*el, sums[1]+el]}
# => [720, 21]
That is, inject the array with multiple initial values for the results and then calculate new value for every element.

Resources