How to improve this Ruby case switch statement? - ruby

I am wondering if there's a more elegant way to say this in Ruby:
FREE_PLAN_MAXIMUM = 1
BASIC_PLAN_MAXIMUM = 10
PREMIUM_PLAN_MAXIMUM = 100
def maximum_entries_per_month
case plan
when "premium"
PREMIUM_PLAN_MAXIMUM
when "basic"
BASIC_PLAN_MAXIMUM
else
FREE_PLAN_MAXIMUM
end
end
I don't like the repetition of premium and basic inside the function. What might be an alternative?

It depends on the rest of your code, especially whether you're using those constants in other places. One pattern I've found nice for this kind of thing is a hash, though.
PLAN_MAXIMUMS = { free: 1, basic: 10, premium: 100 }
def maximum_entries_per_month
PLAN_MAXIMUMS[plan.to_sym] || PLAN_MAXIMUMS[:free]
end

Use Hash#fetch, which allows for a default value, instead of a case statement.
PLAN_MAXIMUMS = { free: 1, basic: 10, premium: 100 }
def maximum_entries_per_month
PLAN_MAXIMUMS.fetch(plan.to_sym, PLAN_MAXIMUMS[:free])
end

You don't need a method. Just have a hash:
maximum_entries_per_month = Hash.new(1).merge{"premium" => 100, "basic" => 10}
and call:
maximum_entries_per_month[plan]

what about:
FREE_PLAN_MAXIMUM = 1
BASIC_PLAN_MAXIMUM = 10
PREMIUM_PLAN_MAXIMUM = 100
PLANS = {'premium' => PREMIUM_PLAN_MAXIMUM, 'basic' => BASIC_PLAN_MAXIMUM, 'free' => FREE_PLAN_MAXIMUM}
def maximum_entries_per_month
PLANS[plan] or FREE_PLAN_MAXIMUM
end
that "or FREE_PLAN_MAXIMUM" will catch any plan that's not "premium", "basic" or "free", if you are sure you only have those three plans just remove that part
EDIT: this way you keep your other constants working
EDIT2: if you don't want to add more constants and you are sure plan is one of those, you can do:
def maximum_entries_per_month
self.class.const_get("#{plan.upcase}_PLAN_MAXIMUM")
end

Related

Sort a array of string by the reverse value

Right now I've produced the following code to sort a list of domains
domains = [
'api.test.google.com',
'dev.blue.google.com',
'dev.test.google.com',
'a.blue.google.com'
]
filtered = []
domains.each { |domain| filtered.push domain.reverse! }
domains.sort!
domains.each { |domain| filtered.push domain.reverse! }
The output of this code will be:
["a.blue.google.com", "dev.blue.google.com", "api.test.google.com", "dev.test.google.com"]
I'm trying to find a way to make this more elegant as it does not look like the most optimal solution to solve this problem but I'm having issues figuring out what is.
Thank you for your help!
Would this work for you?
domains.
map{|d| d.split(".")}.
sort_by(&:reverse).
map{|d| d.join(".") }
Edit: or indeed
domains.sort_by{|x| x.split(".").reverse}
Just to add, I think that something like this deserves to be a value object, as these are not simply strings and they have their own attributes and special behaviour (such as this sort).
For example:
class Domain
include Comparable
def initialize(string)
#string = string
end
def to_s
#string
end
def elements
#string.split(".")
end
protected def <=>(other)
elements.reverse <=> other.elements.reverse
end
def tld
elements.last
end
end
So you can then:
domains = [
Domain.new('api.test.google.com'),
Domain.new('dev.blue.google.com'),
Domain.new('dev.test.google.com'),
Domain.new('a.blue.google.com'),
]
domains.map(&:to_s)
=> ["api.test.google.com", "dev.blue.google.com", "dev.test.google.com", "a.blue.google.com"]
domains.sort.map(&:to_s)
=> ["a.blue.google.com", "dev.blue.google.com", "api.test.google.com", "dev.test.google.com"]
You can also add in any other behaviour you like, such as a method for returning the top level domain.
If all you want to do is sort by the reversed value use sort_by:
domains = [
'api.test.google.com',
'dev.blue.google.com',
'dev.test.google.com',
'a.blue.google.com'
]
domains.sort_by { |domain| domain.reverse }
#=> ["a.blue.google.com", "dev.blue.google.com", "api.test.google.com", "dev.test.google.com"]
If you are concerned with keeping the strings between the dots in the original order you can use:
domains.sort_by { |domain| domain.split('.').reverse }
#=> ["a.blue.google.com", "dev.blue.google.com", "api.test.google.com", "dev.test.google.com"]

get nextPageToken within my ruby program

I made this little program in Ruby, it permits to get all my liked videos. But since the maxResult is 50 and i have 1200 videos i need to use the nextPageToken.
But i don't inderstand how to use it in my program.
this is the code :
def playlist_items_list_by_playlist_id(service, part, **params)
params = params.delete_if { |p, v| v == ''}
response = service.list_playlist_items(part, params)
print_results(response)
end
playlist_items_list_by_playlist_id(service, 'snippet',
max_results: 50,
playlist_id: 'LLkyneo6XAv1no1jH33K2Bmg')
I tried to put :pageToken => next_page_tokenbeside the the playlist_id line but it is not working.
Can someone help me ?
Thank you

How to get more records out of the Twitter gem?

I'm banging my head trying to understand how the Twitter gem's pagination works.
I've tried max_id and cursor and they both strangely don't work.
Basically the maximum I can get out of search results is 100, and I would like to get 500.
Current code:
max_page = 5
max_id = -1
#data = []
for i in (1..max_page)
t = twt_client.search("hello world", :count => 100, :result_type => :recent, :max_id => max_id)
t.each do | tweet |
#data << tweet
end
max_id = t.next_results[:max_id]
end
This actually tells me that next_results is a private method, anyone has a working solution?
Without knowing which gem you're referencing (please specify a URL), I'd say intiuitively that cursor and max_id wouldn't get you what you want. However count would. Since you say you're only retrieving 100 results and count is set to 100, that would make sense to me.
t = twt_client.search("hello world", :count => 500, :result_type => :recent, :max_id => max_id)
I'm assuming you're talking about the Twitter client referenced here. My first question is: What's twt_client and for that matter, what does its search method return? It's also possible that you've unwittingly updated the gem and there's been a code base change that makes your current script out of date.
Take a look at your installed gem version and another look at the README here.
Twitter::SearchResults#next_results is private, because they try to provide uniform interface for enumeration.
Look, there's included Twitter::Enumerable in search_results.rb
module Twitter
class SearchResults
include Twitter::Enumerable
...
private
def last?
!next_page?
end
...
def fetch_next_page
response = #client.send(#request_method, #path, next_page).body
self.attrs = response
end
...
end
end
And if you look at enumerable.rb, you'll see that method's Twitter::SearchResults#last? and Twitter::SearchResults#fetch_next_page are used by Twitter::SearchResults#each method
module Twitter
module Enumerable
include ::Enumerable
# #return [Enumerator]
def each(start = 0)
return to_enum(:each, start) unless block_given?
Array(#collection[start..-1]).each do |element|
yield(element)
end
unless last?
start = [#collection.size, start].max
fetch_next_page
each(start, &Proc.new)
end
self
end
...
end
end
And Twitter::SearchResults#each will iterate over pages until there's #attrs[:search_metadata][:next_results] in Twitter's responses. So you need to break iteration after you'll reach 500th element.
I think you just need to use each
#data = []
tweet_number = 1
search_results = twt_client.search("hello world", count: 100, result_type: :recent)
search_results.each do |tweet|
#data << tweet
break if tweet_number == 500
end
This post is a result of looking into gem's sources and twitter's api. I could make a mistake somewhere, since I haven't checked my thoughts in console.
Try this (I basically only updated the calculation of the max_id in the loop):
max_page = 5
max_id = -1
#data = []
for i in (1..max_page)
t = twt_client.search("hello world", :count => 100, :result_type => :recent, :max_id => max_id)
t.each do | tweet |
#data << tweet
end
max_id = t.to_a.map(&:id).max + 1 # or may be max_id = t.map(&:id).max + 1
end

Assigning multiple values on one line in Ruby using ||=

Now i use the following to make sure all my params values are not nil
start, limit = params[:start] ||= 0, params[:limit] ||= 300
sort, dir = params[:sort] ||= 'id', params[:dir] ||= 'ASC'
But i would like to use something like
params[:start], params[:limit] ||= 0, 300
params[:sort], params[:dir] ||= 'id', 'ASC'
but i get the error syntax error, unexpected tOP_ASGN, expecting '='
Has someone a better way of doing this ?
How about:
params = {start: 0, limit: 300, sort: 'id', dir: 'ASC'}.merge(params)
Update:
The code above will work if params do not have specified keys, however if e.g. params[:key] exists and associated value is nil, nil will be the result. To fix that you can do:
params = {start: 0, limit: 300, sort: 'id', dir: 'ASC'}.merge(params) {|_,old,new| new || old }
While writing code like:
start, limit = params[:start] ||= 0, params[:limit] ||= 300
sort, dir = params[:sort] ||= 'id', params[:dir] ||= 'ASC'
or:
params[:start], params[:limit] ||= 0, 300
params[:sort], params[:dir] ||= 'id', 'ASC'
might seem desirable because it's "tight", "concise" or "terse", it veers into the oncoming-traffic lane and becomes unreadable.
It's OK to spread out logic, because the goal is to make our code understandable to those who might have to maintain it at 3:00AM on Saturday night.
Instead of the above, I'd break it apart:
start = params[:start] ||= 0 # explain why 0
limit = params[:limit] ||= 300 # explain why 300
sort = params[:sort] ||= 'id' # explain why 'id'
dir = params[:dir] ||= 'ASC' # explain why 'ASC'
or:
start, limit, sort, dir = params.values_at(:start, :limit, :sort, :dir)
start ||= 0 # explain why 0
limit ||= 300 # explain why 300
sort ||= 'id' # explain why 'id'
dir ||= 'ASC' # explain why 'ASC'
In either of the above cases, I'd go one step farther to define constants to use symbolic names, rather than hard-coded "magic" values.
You might gain a millisecond of execution speed writing code like you want, if you can get it to work, but you can lose seconds or minutes of maintenance time, and irritate coworkers or others using your code because it's not normal, standard, or expected, so don't go there.
I ended up doing it like this, which is concise and readable to me.
I need to pass the params as a whole because there might be other values there i need to build my selection.
The variable names speak for themselves and they might end up in a yaml configuration file with alle the explanation they need.
def generic_data_getter (class_name, params, start=0, limit=300, sort='id', dir='ASC')
selection = build_selection(class_name, params)
data = class_name.where(selection).offset(start).limit(limit).order("#{sort} #{dir}")
{:success => true, :totalCount => data.except(:offset, :limit, :order).count, :result => data.to_hash}
end
def get_data_for (class_name, params)
generic_data_getter(class_name, params, params[:start], params[:limit], params[:sort], params[:dir])
end

meta-ruby: how to dynamically call a scoped constant?

Say I have a class like:
class Person
module Health
GOOD = 10
SICK = 4
DEAD = 0
end
end
I can reference such Health codes like: Person::Health::GOOD. I'd like to dynamically generate a hash that maps from number values back to constant names:
{ 10 => "GOOD",
4 => "SICK",
0 => "DEAD" }
To do this dynamically, I've come up with:
Person::Health.constants.inject({}) do |hsh, const|
hsh.merge!( eval("Person::Health::#{const}") => const.to_s )
end
This works, but I wonder if there's a better/safer way to go about it. It's in a Rails app, and while it's nowhere near any user input, eval still makes me nervous. Is there a better solution?
You can use constants and const_get for this purpose.
ph = Person::Health # Shorthand
Hash[ph.constants(false).map { |c| [ph.const_get(c), c.to_s ] }]
# {10=>:GOOD, 4=>:SICK, 0=>:DEAD}
I added false to .constants to prevent including any inherited constants from included Modules. For example, without false the following scenario would also include a 5 => "X" mapping:
module A
X = 5
end
class Person
module Health
include A
# ...
end
end
Hash[ph.constants.map { |c| [ph.const_get(c), c.to_s ] }]
# {10=>"GOOD", 4=>"SICK", 0=>"DEAD", 5=>"X"}

Resources