Ordering a rails model with string outline notation - ruby-on-rails

I have a rails model that the primary field that the user wants to sort on is a Line Item that is stored in dot-notation format as a string (i.e.: 2.1.4, 2.1.4.1, 2.1.4.5, etc). Ordering alphabetically works great, except that 2.1.4.10 comes before 2.1.4.2 alphabetically. What I want to call 'dot-based numeric order' would put 2.1.4.10 after 2.1.4.9, and 2.4.1.10.1 would precede 2.4.1.11
The question is this: What is The Rails Way™ to set the default order on the model so that the Line Items appear in the correct order, according to 'dot-based numeric order'.
Presume a simple case:
class LineItem < ActiveRecord::Base
validates :line_item, :presence => true, :uniqueness => true
end
and that :line_item is a string.

I assume you are using PostgreSQL and if you really want to set default order for your model, add this default_scope to your LineItem model:
default_scope -> { order("STRING_TO_ARRAY(line_item, '.')::int[] ASC") }
Otherwise I suggest you to use named scope, it can be override and chained:
scope :version_order, -> { order("STRING_TO_ARRAY(line_item, '.')::int[] ASC") }

To do this yourself:
lines = ['3.3.3.3', '3.54.3.3', '3.3.3.20']
sorted = lines.sort do |a, b|
a.split('.').zip(b.split('.')).inject(0) do |res, val|
(res == 0)? val[0].to_i <=> val[1].to_i : res
end
end #=> ["3.3.3.3", "3.3.3.20", "3.54.3.3"]
How it works:
To sort, we pass an array and a block, that blocks gives us 2 arguments that are next to each other in the list, and we can return 0, -1, or 1, which tells Ruby which directions to swap the numbers.
[4,3,-1,2].sort do |x, y|
if x > y
1
elsif x < y
-1
else
0
end
end #=> [-1, 2, 3, 4]
Instead of doing that long logic, Ruby provides a nice operator for us: <=>. Zero means no change, -1 means it's in ascending order, and 1 means the two numbers are in descending order. Ruby repeats that a bunch, and sorts the list.
4 <=> 4 #=> 0
3 <=> 5 #=> -1
5 <=> 3 #=> 1
7 <=> -1 #-> 1
So, we should give higher items (in terms of dots) priority:
#Pseudo Code:
33.44 > 22.55 #=> true
33.44 < 44.33
The easiest way to integrate through all the numbers is an #inject, which gives you a value, and the item you are on. You can do things like this:
[4,4,4].inject(0) {|sum, i| sum + i} #=> 12
[4,4,4].inject(0) {|sum, i| sum - i} #=> -12
['Hello', "I'm penne12"] {|new_word, word| new_word + "-" + word} #=> "Hello-I'm penne12"
So, we'll use an inline if:
(true)? "it's true" : "true is now false. Yay!" #=> "it's true"
(4 > 5)? "logic is weird" : "4 > 5" #=> "4 > 5"
Like this:
.inject(0) do |res, val|
(res == 0)? val[0].to_i <=> val[1].to_i : res
end
We'll split both strings by the ., to get a list:
"Hello. This. Is. A. Test.".split('.') #=> ["Hello", " This", " Is", " A", "Test"]
"4.4.4.4" #=> [4,4,4,4]
And join the two lists together by element using ruby's #Zip (it's really weird.)
[4,4,4,4].zip([5,5,5,5]) #=> [[4,5], [4,5], [4,5], [4,5]]
You can change what item a and b are, if you want to sort by a different property. Ruby doesn't care what you do to either variable, it only cares about the return value.
a, b = a.line_item, b.line_item
On a model:
class LineItem < ActiveRecord::Base
validates :line_item, :presence => true, :uniqueness => true
def self.sort_by_dbno
self.all.sort do |a, b|
a, b = a.line_item, b.line_item
a.split('.').zip(b.split('.')).inject(0) do |res, val|
(res == 0)? val[0].to_i <=> val[1].to_i : res
end
end
end
end

I overrode the <=> operator with #Penne12's code:
def <=>(y)
self.line_item.split('.').zip(y.line_item.split('.')).inject(0) do |res, val|
(res == 0)? val[0].to_i <=> val[1].to_i : res
end
end
Sorting works on any enumerable collection, with no sort block:
bobs_items = LineItem.where(:owner => bob, :complete => false)
"Bob's workload: #{bobs_items.sort.map { |li| li.line_item }.join(', ')}"

Related

Ruby on rails: function to find value by key

I just encountered a problem with ruby syntax:
The enum example is:
class AaaBbb < ApplicationRecord
enum number: { a: 1, b: 2, c: 3, d: 5 }
or
class AaaBbb < ApplicationRecord
enum number: { "a" => 1, "b" => 2, "c" => 3, "d" => 5 }
The function is:
def find_value
AaaBbb.numbers.each do |key, value|
puts "#{key} = #{value}"
if key == AaaBbb.numbers[:key] (WRONG CODE HERE, NEED TO FIX)
return value
else
return 0
end
end
end
So I am trying to write a function that if it finds the key, then return the value.
You use AaaBbb.numbers[:key] instead of AaaBbb.numbers[key] .. that is, you're passing the symbol :key instead of the actual value key.
Also, looks like you have a second problem. Your loop will always end after running the first time. This is because return in Ruby isn't scoped to the each. It will return from the method, e.g. ending the loop immediately.
But really, I would just rewrite this method using simpler logic. I don't think you need a loop here at all.
def find_value(key)
val = AaaBbb.numbers[key] # this will be nil if the key isn't found
val || 0
end

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 apply multiple regular expression on single field

Currently I have a regular expression for zip-codes for the U.S.:
validates :zip,
presence: true,
format: { with: /\A\d{5}(-\d{4})?\z/ }
I want to use different regular expressions for other countries on the same zip-code so the regular expression should be used according to the country:
For Australia 4 digits
For Canada 6 digits alphanumeric
For UK 6-7 digits alphanumeric
Can someone suggest how can I full fill my requirement?
You can give a lambda that returns a Regexp as the :with option for the format validator (see :with), which makes this nice and clean:
ZIP_COUNTRY_FORMATS = {
'US' => /\A\d{5}(-\d{4})?\z/,
'Australia' => /\A\d{4}\z/,
# ...
}
validates :zip, presence: true,
format: { with: ->(record){ ZIP_COUNTRY_FORMATS.fetch(record.country) } }
Note that uses Hash#fetch instead of Hash#[] so that if a country that doesn't exist is given it will raise a KeyError just as a sanity check. Alternatively you could return a default Regexp that matches anything:
ZIP_COUNTRY_FORMATS.fetch(record.country, //)
...or nothing:
ZIP_COUNTRY_FORMATS.fetch(record.country, /.\A/)
...depending on the behavior you want.
You would want to write a method to help you:
validates :zip, presence: true, with: :zip_validator
def zip_validator
case country
when 'AU'
# some regex or fail
when 'CA'
# some other regex or fail
when 'UK'
# some other regex or fail
else
# should this fail?
end
end
Suppose we give examples of valid postal codes for each country in a hash such as the following.
example_pcs = {
US: ["", "98230", "98230-1346"],
CAN: ["*", "V8V 3A2"],
OZ: ["!*", "NSW 1130", "ACT 0255", "VIC 3794", "QLD 4000", "SA 5664",
"WA 6500", "TAS 7430", "NT 0874"]
}
where the first element of each array is a string of codes that will be explained later.
We can construct a regex for each country from this information. (The information would undoubtedly be different in a real application, but I am just presenting the general idea.) For each country we construct a regex for each example postal code, using in part the above-mentioned codes. We then take the union of those regexes to obtain a single regex for that country. Here's one way the regex for an example postal code might be constructed.
def make_regex(str, codes='')
rstr = str.each_char.chunk do |c|
case c
when /\d/ then :DIGIT
when /[[:alpha:]]/ then :ALPHA
when /\s/ then :WHITE
else :OTHER
end
end.
map do |type, arr|
case type
when :ALPHA
if codes.include?('!')
arr
elsif arr.size == 1
"[[:alpha:]]"
else "[[:alpha:]]\{#{arr.size}\}"
end
when :DIGIT
(arr.size == 1) ? "\\d" : "\\d\{#{arr.size}\}"
when :WHITE
case codes
when /\*/ then "\\s*"
when /\+/ then "\\s+"
else (arr.size == 1) ? "\\s" : "\\s\{#{arr.size}\}"
end
when :OTHER
arr
end
end.
join
Regexp.new("\\A" << rstr << "\\z")
end
I've made the regex case-insensitive for letters, but that could of course be changed. Also, for some countries, the regex produced may have to be tweaked manually and/or some pre- or post-processing of postal code strings may be called for. For example, some combinations may have the correct format but nonetheless are not valid postal codes. In Australia, for example, the four digits following each region code must fall within specified ranges that vary by region.
Here are some examples.
make_regex("12345")
#=> /\A\d{5}\z/
make_regex("12345-1234")
#=> /\A\d{5}-\d{4}\z/
Regexp.union(make_regex("12345"), make_regex("12345-1234"))
#=> /(?-mix:\A\d{5}\z)|(?-mix:\A\d{5}-\d{4}\z)/
make_regex("V8V 3A2", "*")
#=> /\A[[:alpha:]]\d[[:alpha:]]\s*\d[[:alpha:]]\d\z/
make_regex("NSW 1130", "!*")
# => /\ANSW\s*\d{4}\z/
Then, for each country, we take the union of the regexes for each example postal code, saving those results as values in a hash whose keys are country codes.
h = example_pcs.each_with_object({}) { |(country, (codes, *examples)), h|
h[country] = Regexp.union(examples.map { |s| make_regex(s, codes) }.uniq) }
#=> {:US=>/(?-mix:\A\d{5}\z)|(?-mix:\A\d{5}-\d{4}\z)/,
# :CAN=>/\A[[:alpha:]]\d[[:alpha:]]\s*\d[[:alpha:]]\d\z/,
# :OZ=>/(?-mix:\ANSW\s*\d{4}\z)|(?-mix:\AACT\s*\d{4}\z)|(?-mix:\AVIC\s*\d{4}\z)|(?-mix:\AQLD\s*\d{4}\z)|(?-mix:\ASA\s*\d{4}\z)|(?-mix:\AWA\s*\d{4}\z)|(?-mix:\ATAS\s*\d{4}\z)|(?-mix:\ANT\s*\d{4}\z)/}
"12345" =~ h[:US]
#=> 0
"12345-1234" =~ h[:US]
#=> 0
"1234" =~ h[:US]
#=> nil
"12345 1234" =~ h[:US]
#=> nil
"V8V 3A2" =~ h[:CAN]
#=> 0
"V8V 3A2" =~ h[:CAN]
#=> 0
"V8v3a2" =~ h[:CAN]
#=> 0
"3A2 V8V" =~ h[:CAN]
#=> nil
"NSW 1132" =~ h[:OZ]
#=> 0
"NSW 1132" =~ h[:OZ]
#=> 0
"NSW1132" =~ h[:OZ]
#=> 0
"NSW113" =~ h[:OZ]
#=> nil
"QLD" =~ h[:OZ]
#=> nil
"CAT 1132" =~ h[:OZ]
#=> nil
The steps performed in make_regex for
str = "V8V 3A2"
codes = "*+"
are as follows.
e = str.each_char.chunk do |c|
case c
when /\d/ then :DIGIT
when /[[:alpha:]]/ then :ALPHA
when /\s/ then :WHITE
else :OTHER
end
end
#=> #<Enumerator: #<Enumerator::Generator:0x007f9ff201a330>:each>
We can see the values that will be generated by this enumerator by converting it to an array.
e.to_a
#=> [[:ALPHA, ["V"]], [:DIGIT, ["8"]], [:ALPHA, ["V"]], [:WHITE, [" "]],
# [:DIGIT, ["3"]], [:ALPHA, ["A"]], [:DIGIT, ["2"]]]
Continuing,
a = e.map do |type, arr|
case type
when :ALPHA
if codes.include?('!')
arr
elsif arr.size == 1
"[[:alpha:]]"
else "[[:alpha:]]\{#{arr.size}\}"
end
when :DIGIT
(arr.size == 1) ? "\\d" : "\\d\{#{arr.size}\}"
when :WHITE
case codes
when /\*/ then "\\s*"
when /\+/ then "\\s+"
else (arr.size == 1) ? "\\s" : "\\s\{#{arr.size}\}"
end
when :OTHER
arr
end
end
#=> ["[[:alpha:]]", "\\d", "[[:alpha:]]", "\\s*", "\\d", "[[:alpha:]]", "\\d"]
rstr = a.join
#=> "[[:alpha:]]\\d[[:alpha:]]\\s*\\d[[:alpha:]]\\d"
t = "\\A" << rstr << "\\z"
#=> "\\A[[:alpha:]]\\d[[:alpha:]]\\s*\\d[[:alpha:]]\\d\\z"
puts t
#=> \A[[:alpha:]]\d[[:alpha:]]\s*\d[[:alpha:]]\d\z
Regexp.new(t)
#=> /\A[[:alpha:]]\d[[:alpha:]]\s*\d[[:alpha:]]\d\z/

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

Best way to analyse data using ruby

I would like to analyse data in my database to find out how many times certain words appear.
Ideally I would like a list of the top 20 words used in a particular column.
What would be the easiest way of going about this.
Create an autovivified hash and then loop through the rows populating the hash and incrementing the value each time you get the same key (word). Then sort the hash by value.
A word counter...
I wasn't sure if you were asking how to get rails to work on this or how to count words, but I went ahead and did a column-oriented ruby wordcounter anyway.
(BTW, at first I did try the autovivified hash, what a cool trick.)
# col: a column name or number
# strings: a String, Array of Strings, Array of Array of Strings, etc.
def count(col, *strings)
(#h ||= {})[col = col.to_s] ||= {}
[*strings].flatten.each { |s|
s.split.each { |s|
#h[col][s] ||= 0
#h[col][s] += 1
}
}
end
def formatOneCol a
limit = 2
a.sort { |e1,e2| e2[1]<=>e1[1] }.each { |results|
printf("%9d %s\n", results[1], results[0])
return unless (limit -= 1) > 0
}
end
def formatAllCols
#h.sort.each { |a|
printf("\n%9s\n", "Col " + a[0])
formatOneCol a[1]
}
end
count(1,"how now")
count(1,["how", "now", "brown"])
count(1,[["how", "now"], ["brown", "cow"]])
count(2,["you see", "see you",["how", "now"], ["brown", "cow"]])
count(2,["see", ["see", ["see"]]])
count("A_Name Instead","how now alpha alpha alpha")
formatAllCols
$ ruby count.rb
Col 1
3 how
3 now
Col 2
5 see
2 you
Col A_Name Instead
3 alpha
1 how
$
digitalross answer looks too verbose to me, also, as you tag ruby-on-rails and said you use DB.. i'm assuming you need an activerecord model so i'm giving you a full solution
in your model:
def self.top_strs(column_symbol, top_num)
h = Hash.new(0)
find(:all, :select => column_symbol).each do |obj|
obj.send(column_symbol).split.each do |word|
h[word] += 1
end
end
h.map.sort_by(&:second).reverse[0..top_num]
end
for example, model Comment, column body:
Comment.top_strs(:body, 20)

Resources