Natural sorting of 3-4 characters in Rails - ruby-on-rails

I have an unsorted list of area postcodes as follows:
["E1", "E1C", "E1D", "E10", "E11", "E12", "E2", "E3", "E4", "EC1", "EC1A", "EC1M", "EC1N",
"EC1R", "EC1V", "EC1Y", "EC2", "EC2A", "EC2M", "EC2N", "N1", "N10", "N11", "N12",
"N13", "N2", "NW1", "NW10", "NW2" etc]
I'd like to sort them as follows:
["E1", "E1C", "E1D", "E2", "E3", "E4", "E10", "E11", "E12", "EC1", "EC1A", "EC1M", "EC1N",
"EC1R", "EC1V", "EC1Y", "EC2", "EC2A", "EC2M", "EC2N", "N1", "N2", "N10", "N11", "N12",
"N13", "NW1", "NW2, "NW10" etc]
So to sum up the order of the formats for postcodes beginning with E would be:
E1
E1C
E11
EC1
EC1V
Same order for postcodes beginning with N, etc.
What would be the recommended way of sorting such strings? In this case the format of the string is always known, i.e. it will always be 2-4 alphanumberic characters, the first always being a letter.
Should I order the strings by length first and then order within each length group, or is there a more elegant method?

I'd use
array.sort_by do |str|
/\A(\w)(\d+)\Z/ === str
[$1, $2.to_i]
end
or, if you have arbitrary sequences of alternating letters and digits,
array.sort_by do |str|
/\A(\D*)(\d*)(\D*)(\d*)\Z/.match(str)[1..-1].reject(&:blank?).collect do |item|
/\d/ === item ? item.to_i : item
end
end

Kind of a weird way of doing it, but I think this should work:
array.sort do |a, b|
a = a.dup
b = b.dup
regex = /(\d+)/
a.match(regex)
a_num = $1.to_i
b.match(regex)
b_num = $1.to_i
if a_num > b_num
a.gsub!(regex, "1")
b.gsub!(regex, "0")
elsif a_num < b_num
a.gsub!(regex, "0")
b.gsub!(regex, "1")
end
a <=> b
end

Related

How to remove words that have the same letter more than once?

I am trying to remove words that have more that have the same letter more than once. I have tried squeeze but all that is doing is removing words that have duplicate letters next to each other.
Here is the code at the moment:
array = []
File.open('word.txt').each do |line|
if line.squeeze == line
array << line
end
end
Input from word.txt
start
james
hello
joins
regex
Output that I am looking for
james
joins
Any suggestions on how I can get around this.
Perhaps something like this:
array = []
File.open('word.txt').each do |line|
chars = line.chars
array << line if chars.uniq == chars
end
or shorter:
array = File.open('word.txt').select { |word| word.chars.uniq == word.chars }
You could use a regular expression, for example:
re = /
(.) # match and capture a single character
.*? # any number of characters in-between (non-greedy)
\1 # match the captured character again
/x
Example:
'start'[re] #=> "tart"
'james'[re] #=> nil
'hello'[re] #=> "ll"
'joins'[re] #=> nil
'regex'[re] #=> "ege"
It can be passed to grep to return all matched lines:
IO.foreach('word.txt').grep(re)
#=> ["start\n", "hello\n", "regex\n"]
or to grep_v to return the other lines:
IO.foreach('word.txt').grep_v(re)
#=> ["james\n", "joins\n"]

How to merge 2 strings alternately in rails?

I have 2 strings:
a = "qwer"
b = "asd"
Result = "qawsedr"
Same is the length of b is greater than a. show alternate the characters.
What is the best way to do this? Should I use loop?
You can get the chars from your a and b string to work with them as arrays and then "merge" them using zip, then join them.
In the case of strings with different length, the array values must be reversed, so:
def merge_alternately(a, b)
a = a.chars
b = b.chars
if a.length >= b.length
a.zip(b)
else
array = b.zip(a)
array.map{|e| e != array[-1] ? e.reverse : e}
end
end
p merge_alternately('abc', 'def').join
# => "adbecf"
p merge_alternately('ab', 'zsd').join
# => "azbsd"
p merge_alternately('qwer', 'asd').join
# => "qawsedr"
Sebastián's answer gets the job done, but it's needlessly complex. Here's an alternative:
def merge_alternately(a, b)
len = [a.size, b.size].max
Array.new(len) {|n| [ a[n], b[n] ] }.join
end
merge_alternately("ab", "zsd")
# => "azbsd"
The first line gets the size of the longer string. The second line uses the block form of the Array constructor; it yields the indexes from 0 to len-1 to the block, resulting in an array like [["a", "z"], ["b", "s"], [nil, "d"]]. join turns it into a string, conveniently calling to_s on each item, which turns nil into "".
Here's another version that does basically the same thing, but skips the intermediate arrays:
def merge_alternately(a, b)
len = [a.size, b.size].max
len.times.reduce("") {|s, i| s + a[i].to_s + b[i].to_s }
end
len.times yields an Enumerator that yields the indexes from 0 to len-1. reduce starts with an empty string s and in each iteration appends the next characters from a and b (or ""—nil.to_s—if a string runs out of characters).
You can see both on repl.it: https://repl.it/I6c8/1
Just for fun, here's a couple more solutions. This one works a lot like Sebastián's solution, but pads the first array of characters with nils if it's shorter than the second:
def merge_alternately(a, b)
a, b = a.chars, b.chars
a[b.size - 1] = nil if a.size < b.size
a.zip(b).join
end
And it wouldn't be a Ruby answer without a little gsub:
def merge_alternately2(a, b)
if a.size < b.size
b.gsub(/./) { a[$`.size].to_s + $& }
else
a.gsub(/./) { $& + b[$`.size].to_s }
end
end
See these two on repl.it: https://repl.it/I6c8/2

How to expand a string in Ruby based on some condition?

I have a string a5bc2cdf3. I want to expand it to aaaaabcbccdfcdfcdf.
In the string is a5, so the resulting string should contain 5 consecutive "a"s, "bc2" results in "bc" appearing 2 times consecutively, and cdf should repeat 3 times.
If input is a5bc2cdf3, and output is aaaaabcbccdfcdfcdf how can I do this in a Ruby method?
def get_character("compressed_string",index)
expanded_string = calculate_expanded_string(compressed_string)
required_char = expanded_string(char_at, index_number(for eg 3))
end
def calculate_expanded_string(compressed_string)
return expanded
end
You may use a regex like
.gsub(/([a-zA-Z]+)(\d+)/){$1*$2.to_i}
See the Ruby online demo
The /([a-zA-Z]+)(\d+)/ will match stubstrings with 1+ letters (([a-zA-Z]+)) and 1+ digits ((\d+)) and will capture them into 2 groups that are later used inside a block to return the string you need.
Note that instead of [a-zA-Z] you might consider using \p{L} that can match any letters.
You want to break out of gsub once the specified index is reached in the original "compressed" string. It is still possible, see this Ruby demo:
s = 'a5bc2cdf3' # input string
index = 5 # break index
result = "" # expanded string
s.gsub!(/([a-zA-Z]+)(\d+)/){ # regex replacement
result << $1*$2.to_i # add to the resulting string
break if Regexp.last_match.end(0) >= index # Break if the current match end index is bigger or equal to index
}
puts result[index] # Show the result
# => b
For brevity, you may replace Regexp.last_match with $~.
I would propose to use scan to move over the compressed string, using a simple RegEx which detects groups of non-decimal characters followed by their count as decimal /([^\d]+)(\d+)/.
def get_character(compressed_string, index)
result = nil
compressed_string.scan(/([^\d]+)(\d+)/).inject(0) do |total_length, (chars, count)|
decoded_string = chars * count.to_i
total_length += decoded_string.length
if index < total_length
result = decoded_string[-(total_length - index)]
break
else
total_length
end
end
result
end
Knowing the current (total) length, one can break out of the loop if the current expanded string includes the requested index. The string is never decoded entirely.
This code gives the following results
get_character("a5bc2cdf3", 5) # => "b"
get_character("a5bc2cdf3", 10) # => "d"
get_character("a5bc2cdf3", 20) # => nil
Just another way. I prefer Wiktor's method by a long way.
def stringy str, index
lets, nums = str.split(/\d+/), str.split(/[a-z]+/)[1..-1].map(&:to_i)
ostr = lets.zip(nums).map { |l,n| l*n }.join
ostr[index]
end
str = 'a5bc2cdf3'
p stringy str, 5 #=> "b"
I'd use:
str = "a5bc2cdf3"
str.split(/(\d+)/).each_slice(2).map { |s, c| s * c.to_i }.join # => "aaaaabcbccdfcdfcdf"
Here's how it breaks down:
str.split(/(\d+)/) # => ["a", "5", "bc", "2", "cdf", "3"]
This works because split will return the value being split on if it's in a regex group: /(\d+)/.
str.split(/(\d+)/).each_slice(2).to_a # => [["a", "5"], ["bc", "2"], ["cdf", "3"]]
The resulting array can be broken into the string to be repeated and its associated count using each_slice(2).
str.split(/(\d+)/).each_slice(2).map { |s, c| s * c.to_i } # => ["aaaaa", "bcbc", "cdfcdfcdf"]
That array of arrays can then be processed in a map that uses String's * to repeat the characters.
And finally join concatenates all the resulting expanded strings back into a single string.

Ruby on Rails - Change Order Behavior

I'm having a minor problem with how RoR behaves when I tell it to display a result in a certain order.
I have a table called categories that contains a code column. Values in this code column include 1, 6, 12A, and 12B. When I tell the system to display the result (a dropdown) by order according to a foreign key id number and then a code value, it lists the codes in the order of 1, 12A, 12B, and 6 (which ideally should be 1, 6, 12A, and 12B).
Here is the dropdown:
collection_select(:category, :category_id, Category.order(:award_id, :code), :id, :award_code_category)
I know part of the problem is the A and B part of those two codes, I can't treat them as strict integers (code is a string type in the table).
I would love any thoughts about this.
steakchaser's answer called on:
['1', '12B', '12A', '6']
would return
['1', '6', '12B', '12A']
You lose the ordering of the letters.
You could create a helper to do the sorting:
def self.sort_by_category_codes(categories)
sorted_categories = categories.sort do |cat1, cat2|
if cat1.award_id == cat2.award_id
# award id matches so compare by code
if cat1.code.to_i == cat2.code.to_i
# the leading numbers are the same (or no leading numbers)
# so compare alphabetically
cat1.code <=> cat2.code
else
# there was a leading number so sort numerically
cat1.code.to_i <=> cat2.code.to_i
end
else
# award ids were different so compare by them
cat1.award_id <=> cat2.award_id
end
end
return sorted_categories
end
Both ['1', '12A', '12B', '6'] and ['1', '12B', '12A', '6'] would return ['1', '6', '12A', '12B']
Then call:
collection_select(:category, :category_id, sort_by_category_codes(Category.all), :id, :award_code_category)
The only issue I see with my solution is that codes that start with letters such as just 'A' would be returned ahead of numbers. If you need 'A' to be returned after '1A' you'll need some extra logic in helper method.
You could use a regex (most flexible depending on the pattern you really need to find) as part of the sorting to extract out the numeric portion:
['1', '12A', '12B', '6'].sort{|c1, c2| c1[/\d*(?:\.\d+)?/].to_i <=> c2[/\d*(?:\.\d+)?/].to_i}
Deleting non-integers when sorting is also a little easier to read:
['1', '12A', '12B', '6'].sort{|c1, c2| c1.delete("^0-9.").to_i <=> c2.delete("^0-9.").to_i}
To sort an array of those values you would:
["1", "6", "12A", "12B"].sort do |x, y|
res = x.to_i <=> y.to_i
res = x <=> y if res == 0
res
end
To get the categories sorted in that order could do something like:
categories = Category.all.sort do |x, y|
res = x.code.to_i <=> y.code.to_i
res = x.code <=> y.code if res == 0
res
end
From your code I inferred that you may want to sort on award_id with a second order sort on code. That would look like:
categories = Category.all.sort do |x, y|
res = x.award_id <=> y.award_id
res = x.code.to_i <=> y.code.to_i if res == 0
res = x.code <=> y.code if res == 0
res
end

ruby looping question

I want to make a loop on a variable that can be altered inside of the loop.
first_var.sort.each do |first_id, first_value|
second_var.sort.each do |second_id, second_value_value|
difference = first_value - second_value
if difference >= 0
second_var.delete(second_id)
else
second_var[second_id] += first_value
if second_var[second_id] == 0
second_var.delete(second_id)
end
first_var.delete(first_id)
end
end
end
The idea behind this code is that I want to use it for calculating how much money a certain user is going to give some other user. Both of the variables contain hashes. The first_var is containing the users that will get money, and the second_var is containing the users that are going to pay. The loop is supposed to "fill up" a user that should get money, and when a user gets full, or a user is out of money, to just take it out of the loop, and continue filling up the rest of the users.
How do I do this, because this doesn't work?
Okay. What it looks like you have is two hashes, hence the "id, value" split.
If you are looping through arrays and you want to use the index of the array, you would want to use Array.each_index.
If you are looping through an Array of objects, and 'id' and 'value' are attributes, you only need to call some arbitrary block variable, not two.
Lets assume these are two hashes, H1 and H2, of equal length, with common keys. You want to do the following: if H1[key]value is > than H2[key]:value, remove key from H2, else, sum H1:value to H2:value and put the result in H2[key].
H1.each_key do |k|
if H1[k] > H2[k] then
H2.delete(k)
else
H2[k] = H2[k]+H1[k]
end
end
Assume you are looping through two arrays, and you want to sort them by value, and then if the value in A1[x] is greater than the value in A2[x], remove A2[x]. Else, sum A1[x] with A2[x].
b = a2.sort
a1.sort.each_index do |k|
if a1[k] > b[k]
b[k] = nil
else
b[k] = a1[k] + b[k]
end
end
a2 = b.compact
Based on the new info: you have a hash for payees and a hash for payers. Lets call them ees and ers just for convenience. The difficult part of this is that as you modify the ers hash, you might confuse the loop. One way to do this--poorly--is as follows.
e_keys = ees.keys
r_keys = ers.keys
e = 0
r = 0
until e == e_keys.length or r == r_keys.length
ees[e_keys[e]] = ees[e_keys[e]] + ers[r_keys[r]]
x = max_value - ees[e_keys[e]]
ers[r_keys[r]] = x >= 0 ? 0 : x.abs
ees[e_keys[e]] = [ees[e_keys[e]], max_value].min
if ers[r_keys[r]] == 0 then r+= 1 end
if ees[e_keys[e]] == max_value then e+=1 end
end
The reason I say that this is not a great solution is that I think there is a more "ruby" way to do this, but I'm not sure what it is. This does avoid any problems that modifying the hash you are iterating through might cause, however.
Do you mean?
some_value = 5
arrarr = [[],[1,2,5],[5,3],[2,5,7],[5,6,2,5]]
arrarr.each do |a|
a.delete(some_value)
end
arrarr now has the value [[], [1, 2], [3], [2, 7], [6, 2]]
I think you can sort of alter a variable inside such a loop but I would highly recommend against it. I'm guessing it's undefined behaviour.
here is what happened when I tried it
a.each do |x|
p x
a = []
end
prints
1
2
3
4
5
and a is [] at the end
while
a.each do |x|
p x
a = []
end
prints nothing
and a is [] at the end
If you can I'd try using
each/map/filter/select.ect. otherwise make a new array and looping through list a normally.
Or loop over numbers from x to y
1.upto(5).each do |n|
do_stuff_with(arr[n])
end
Assuming:
some_var = [1,2,3,4]
delete_if sounds like a viable candidate for this:
some_var.delete_if { |a| a == 1 }
p some_var
=> [2,3,4]

Resources