Rails loop through 'A..Z' and beyond? - ruby-on-rails

I need to generate a unique name based on a start_date of a model. I have the following code that's working. However, as you can see, it only supports up to letter Z. Is there a way that I can make it work for beyond Z, like this sequence? A,B,C,...X,Y,Z,AA,AB,AC,AD,AE...AZ,BA,BB,BC...
def generate_name
("A".."Z").each do |letter|
random_token = start_date.strftime("%m%d%y") + letter
break random_token unless Tour.where(name: random_token).exists?
end
end

def letters
('a'..'z').each { |i| yield i }
letters do |i|
('a'..'z').each { |j| yield i+j }
end
end
def generate_name
letters do |letter|
random_token = start_date.strftime("%m%d%y") + letter
break random_token unless Tour.where(name: random_token).exists?
end
end

Sam's answer looks like exactly what you requested but here's another possibly useful way to look at your problem:
Think of your string as a number in base 26, where the digits are represented by 'a'..'z'. The only difference between doing it this way and the way you requested is that the first two-digit number (err, two-letter string) will be ba rather than aa (assuming that you started counting with a = 0, b = 1, etc.) and you won't generate any strings starting with a (other than the first one) for the same reason you don't write whole numbers starting with 0 (other than the first one).
If it's useful for you to calculate very quickly that the name dw was the 100th one you generated, then this approach has merit. It's also a good way to work the word hexavigesimal into conversations or at least into documentation.

Thanks Sunxperous.
This worked:
def generate_name
("A".."ZZZ").each do |letter|
random_token = start_date.strftime("%m%d%y") + letter
break random_token unless Tour.where(name: random_token).exists?
end
end

You are looking for Kleene star. Not tested, but seems that RLTK gem can give you such functionality.

Related

Ruby regex puncuation

I am having trouble writing this so that it will take a sentence as an argument and perform the translation on each word without affecting the punctuation.
I'd also like to continue using the partition method.
It would be nice if I could have it keep a quote together as well, such as:
"I said this", I said.
would be:
"I aidsay histay", I said.
def convert_sentence_pig_latin(sentence)
p split_sentence = sentence.split(/\W/)
pig_latin_sentence = []
split_sentence.each do |word|
if word.match(/^[^aeiou]+/x)
pig_latin_sentence << word.partition(/^[^aeiou]+/x)[2] + word.partition(/^[^aeiou]+/x)[1] + "ay"
else
pig_latin_sentence << word
end
end
rejoined_pig_sentence = pig_latin_sentence.join(" ").downcase + "."
p rejoined_pig_sentence.capitalize
end
convert_sentence_pig_latin("Mary had a little lamb.")
Your main problem is that [^aeiou] matches every character outside that range, including spaces, commas, quotation marks, etc.
If I were you, I'd use a positive match for consonants, ie. [b-df-hj-np-tv-z] I would also put that regex in a variable, so you're not having to repeat it three times.
Also, in case you're interested, there's a way to make your convert_sentence_pig_latin method a single gsub and it will do the whole sentence in one pass.
Update
...because you asked...
sentence.gsub( /\b([b-df-hj-np-tv-z])(\w+)/i ) { "#{$2}#{$1}ay" }
# iterate over and replace regexp matches using gsub
def convert_sentence_pig_latin2(sentence)
r = /^[^aeiou]+/i
sentence.gsub(/"([^"]*)"/m) {|x| x.gsub(/\w+/) {|y| y =~ r ? "#{y.partition(r)[2]}#{y.partition(r)[1]}ay" : y}}
end
puts convert_sentence_pig_latin2('"I said this", I said.')
# define instance method: String#to_pl
class String
R = Regexp.new '^[^aeiou]+', true # => /^[^aeiou]+/i
def to_pl
self.gsub(/"([^"]*)"/m) {|x| x.gsub(/\w+/) {|y| y =~ R ? "#{y.partition(R)[2]}#{y.partition(R)[1]}ay" : y}}
end
end
puts '"I said this", I said.'.to_pl
sources:
http://www.ruby-doc.org/core-2.1.0/Regexp.html
http://ruby-doc.org/core-2.0/String.html#method-i-gsub

Ruby: Splitting string on last number character

I have the following method in my Ruby model:
Old:
def to_s
numbers = self.title.scan(/\d+/) if self.title.scan(/\d+/)
return numbers.join.insert(0, "#{self.title.chop} ") if numbers
"#{self.title.titlecase}"
end
New:
def to_s
numbers = self.title.scan(/\d+/)
return numbers.join.insert(0, "#{self.title.sub(/\d+/, '')} ") if numbers.any?
self.title.titlecase
end
A title can be like so: Level1 or TrackStar
So TrackStar should become Track Star and Level1 should be come Level 1, which is why I am doing the scan for numbers to begin with
I am trying to display it like Level 1. The above works, I was just curious to know if there was a more eloquent solution
Try this:
def to_s
self.title.split(/(?=[0-9])/, 2).join(" ")
end
The second argument to split is to make sure a title like "Level10" doesn't get transformed into "Level 1 0".
Edit - to add spaces between words as well, I'd use gsub:
def to_s
self.title.gsub(/([a-z])([A-Z])/, '\1 \2').split(/(?=\d)/, 2).join(" ")
end
Be sure to use single-quotes in the second argument to gsub.
How about this:
'Level1'.split(/(\d+)/).join(' ')
#=> "Level 1"

How can I speed up this Rails code?

It's a vague question I know....but the performance on this block of code is horrible. It takes about 15secs from the original post to the action to rendering the page...
The purpose of this action is to retrieve all Occupations from a CV, all the skills from that CV and the occupations. They need to be organized in 2 arrays:
the first array contains all the Occupations (no duplicates) and has them ordered according their score. Fo each double entry found the score is increased by 1
the second array contains ALL the skills from both the occupation array and the cv. Again no doubles are allowed, but for every double encountered the score of the existing is increased by one.
Below is the code block that performs this operation. It's relatively big compared to my other code snippets, but i hope it's understandable. I know working with the arrays like i do is confusing, but here is what each array location means:
position 0 : the actuall skill/occupation object
position 1 : the score of the entry
position 2 : the location found in the db
position 3 : the location found in the cv
def categorize
#cv = Cv.find(params[:cv_id], :include => [:desired_occupations, :past_occupations, :educational_skills])
#menu = :second
#language = Language.resolve(:code => :en, :name => :en)
#occupation_hashes = []
#skill_hashes = []
(#cv.desired_occupations + #cv.past_occupations).each do |occupation|
section = []
section << 'Desired occupation' if #cv.desired_occupations.include? occupation
section << 'Work experience' if #cv.past_occupations.include? occupation
unless (array = #occupation_hashes.assoc(occupation)).blank?
array[1] += 1
array[2] = (array[2] & section).uniq
else
#occupation_hashes << [occupation, 1, section]
end
occupation.skills.each do |skill|
unless (array = #skill_hashes.assoc skill).blank?
label = occupation.concept.label(#language).value
array[1]+= 1
array[3] << label unless array[3].include? label
else
#skill_hashes << [skill, 1, [], [occupation.concept.label(#language).value]]
end
end
end
#cv.educational_skills.each do |skill|
unless (array = #skill_hashes.assoc skill).blank?
array[1]+= 1
array[3] << 'Education skills' unless array[3].include? 'Education skills'
else
#skill_hashes << [skill, 1, ['Education skills'], []]
end
end
# Sort the hashes
#occupation_hashes.sort! { |x,y| y[1] <=> x[1]}
#skill_hashes.sort! { |x,y| y[1] <=> x[1]}
#max = #skill_hashes.first[1]
#min = #skill_hashes.last[1] end
I can post the additional models and migrations to make it clear what each class does, but I think the first few lines of the above script should be clear on the associations. I'm looking for a way to optimize the each-loops...
That's quite the block of code there. Generally if you're writing methods that serious you're going to have trouble maintaining it in the future. A technique that would help is breaking up that monolithic chunk of code and turning it into a helper class that does the processing in more logical stages, making it easier to fine-tune aspects of it.
For instance, an interface might be:
#categorizer = CvCategorizer.new(params[:cv_id])
This would encapsulate all of the above and save it into instance variables made accessible by being declared with attr_reader.
Using a utility class means you can break up the initialization into steps that are made more clear:
def initialize(cv_id)
# Call a wrapper method that loads the CV
#cv = self.load_cv(cv_id)
# Perform discrete steps to re-order the imported data
self.organize_occupations
self.organize_skills
end
It's really hard to say why this is slow by just looking at it, though I would pay very close attention to log/development.log to see what's going on in there. It could be the initial load is painfully slow but the rest of the method is fine.
You should do a but of profiling in your code to see what is taking a large chunk of time. You can figure out how to work on of the profilers, or just sprinkle some simple puts or logger.info statements throughout your code with a timestamp. Probably easiest to do this by using Benchmark. Note: you may need to require 'benchmark'... not sure if it is auto required in Rails or not.
For a single line, you can do something like this:
logger.info Benchmark.measure { #cv = Cv.find(params[:cv_id], :include => [:desired_occupations, :past_occupations, :educational_skills]) }
And for timing larger blocks of code:
logger.info Benchmark.measure do
(#cv.desired_occupations + #cv.past_occupations).each do |occupation|
section = []
section << 'Desired occupation' if #cv.desired_occupations.include? occupation
section << 'Work experience' if #cv.past_occupations.include? occupation
unless (array = #occupation_hashes.assoc(occupation)).blank?
array[1] += 1
array[2] = (array[2] & section).uniq
else
#occupation_hashes << [occupation, 1, section]
end
end
end
I'd just start with large blocks and then narrow it down. Not knowing how large of a dataset you are dealing with, it is hard to say what the problem zone is.
I'll also concur with others that you will be way better off to break this thing into smaller methods. This will also make it easier to test for performance, as you can do things like:
Benchmark.measure { 10000.times { foo.do_that_thing_that_might_be_slow }}

Simple regex problem in Rails

Ok. It's late and I'm tired.
I want to match a character in a string. Specifically, the appearance of 'a'. As in "one and a half".
If I have a string which is all lowercase.
"one and a half is always good" # what a dumb example. No idea how I thought of that.
and I call titleize on it
"one and a half is always good".titleize #=> "One And A Half Is Always Good"
This is wrong because the 'And' and the 'A' should be lowercase. Obviously.
So, I can do
"One and a Half Is always Good".titleize.tr('And', 'and') #=> "One and a Half Is always Good"
My question: how do I make the "A" an "a" and without making the "Always" into "always"?
This does it:
require 'active_support/all'
str = "one and a half is always good" #=> "one and a half is always good"
str.titleize.gsub(%r{\b(A|And|Is)\b}i){ |w| w.downcase } #=> "One and a Half is Always Good"
or
str.titleize.gsub(%r{\b(A(nd)?|Is)\b}i){ |w| w.downcase } #=> "One and a Half is Always Good"
Take your pick of either of the last two lines. The regex pattern could be created elsewhere and passed in as a variable, for maintenance or code cleanliness.
I like Greg's two-liner (first titleize, then use a regex to downcase selected words.) FWIW, here's a function I use in my projects. Well tested, although much more verbose. You'll note that I'm overriding titleize in ActiveSupport:
class String
#
# A better titleize that creates a usable
# title according to English grammar rules.
#
def titleize
count = 0
result = []
for w in self.downcase.split
count += 1
if count == 1
# Always capitalize the first word.
result << w.capitalize
else
unless ['a','an','and','by','for','in','is','of','not','on','or','over','the','to','under'].include? w
result << w.capitalize
else
result << w
end
end
end
return result.join(' ')
end
end

Sort an array in Ruby ignoring articles ("the", "a", "an")

In my application I need to show a list of songs. Right now I'm doing this:
Song.all.sort {|x,y| x.artist.name <=> y.artist.name }
Unfortunately, this means that "The Notorious B.I.G" will sort with the T's while I want him to sort with the N's (i.e., I want to ignore articles -- "the", "a", and "an" -- for the purposes of sorting.
My first thought was to do this:
Song.all.sort {|x,y| x.artist.name.gsub(/^(the|a|an) /i, '') <=> y.artist.name.gsub(/^(the|a|an) /i, '') }
But it doesn't seem to work. Thoughts?
My favorite approach to these kind of problems is to store an extra sort_order column in the database.
That way when you have 10000 songs that you would like to page through, you can do that in SQL and avoid having to pull them all back.
Its simple to add a before_save filter to keep this column in sync.
The cleanish solution, without schema changes is:
class Artist
def sortable_name
self.name.sub(/^(the|a|an)\s+/i, '')
end
end
class Song
def sortable_name
# note the - is there so [Radio] [head on] and [Radiohead] [karma police]
# are not mixed in the ordering
"#{artist.sortable_name} - #{name}"
end
end
# breaks ties as well
Song.all.sort_by { |song| song.sortable_name }
You may be better off doing this in SQL,
SELECT Title,
CASE WHEN SUBSTRING_INDEX(Title, ' ', 1)
IN ('a', 'an', 'the')
THEN CONCAT(
SUBSTRING(Title, INSTR(Title, ' ') + 1),
', ',
SUBSTRING_INDEX(Title, ' ', 1)
)
ELSE Title
END AS TitleSort
FROM music
ORDER BY TitleSort
There is an article describing this in more detail as well.
The big benefit over the approach you have laid out is that you're pulling ALL of the records out first which will screw you up in subtle ways both performance and user interface wise (I'm thinking of situations like trying to page through a large set of songs).
Your answer seems to be correct but probably u can change it to:
Song.all.sort {|x, y| x.artist.name.sub(/^(the|a|an)\s/i, '') <=> y.artist.name.sub(/^(the|a|an)\s/i, '') }
changed to sub because you only need to change it in the beginning and not the whole string, space changed to \s to indicate a space.
you can go to http://www.rubyxp.com/ to test your regex
if it's still not working then probably some records are not coming out the way they should? like spaces at the start of the string, nil or blank title, etc...
hope it helps =)
Break it up!
class Artist < ActiveRecord::Base
def <=>(other)
name.gsub(/^(the|a|an) /i, '') <=> other.name.gsub(/^(the|a|an) /i, '')
end
end
songs = Song.all(:include => :artist)
songs.sort_by { |s| s.artist }

Resources