How to avoid cyclometic complexity caused by guard blocks? - ruby

I have the following function in one of my classes
def intraday_time_series(opts)
### Guard block ###
valid_resource = opts[:resource] && [:calories, :steps, :distance, :floors, :elevation].include?(opts[:resource])
valid_date = opts[:date]
valid_detail_level = opts[:detailLevel] && %w(1min 15min).include?(opts[:detailLevel])
raise FitgemOauth2::InvalidArgumentError,
'Must specify resource to fetch intraday time series data for.'\
' One of (:calories, :steps, :distance, :floors, or :elevation) is required.' unless valid_resource
raise FitgemOauth2::InvalidArgumentError, 'Must specify the date to fetch intraday time series data for.' unless valid_date
raise FitgemOauth2::InvalidArgumentError,
'Must specify the data resolution to fetch intraday time series data for.'\
' One of (\"1d\" or \"15min\") is required.' unless valid_detail_level
### actual logic ###
resource = opts.delete(:resource)
date = format_date(opts.delete(:date))
detail_level = opts.delete(:detailLevel)
time_window_specified = opts[:startTime] || opts[:endTime]
resource_path = "user/#{#user_id}/activities/"
if time_window_specified
start_time = format_time(opts.delete(:startTime))
end_time = format_time(opts.delete(:endTime))
resource_path += "#{resource}/date/#{date}/1d/#{detail_level}/time/#{start_time}/#{end_time}.json"
else
resource_path += "#{resource}/date/#{date}/1d/#{detail_level}.json"
end
get_call(resource_path)
end
The function has a guard block that checks for any issues with the arguments (the guard block) and if no error is found it does stuff with those arguments.
Now, when I use rubocop to analyze my code, it reports high cyclometic complexity due to the guard blocks. One way I can reduce the complexity is to define another function guard_for_intraday_time_series and move all the guard blocks there. I do not think it as an appropriate solution though because it will populate my code with a guard block for every function that I have in my project.
What is an appropriate way to reduce this complexity, or is it just unavoidable?

You have many intermediate local variables that make the code complicated.
I would refactor it like this:
def intraday_time_series(resource: nil, date: nil,
detailLevel: nil, startTime: nil, endTime: nil)
unless %i[calories steps distance floors elevation].include?(resource)
raise FitgemOauth2::InvalidArgumentError,
"Must specify resource to fetch intraday time series data for."\
" One of (:calories, :steps, :distance, :floors, or :elevation) is required."
end
unless date
raise FitgemOauth2::InvalidArgumentError,
"Must specify the date to fetch intraday time series data for."
end
unless %w(1min 15min).include?(detailLevel)
raise FitgemOauth2::InvalidArgumentError,
"Must specify the data resolution to fetch intraday time series data for."\
" One of (\"1d\" or \"15min\") is required."
end
resource_path = [
"user", #user_id,
"activities", resource,
"date", format_date(date),
"1d", detailLevel
].join("/")
if startTime || endTime
resource_path =
[resource_path, "time", format_time(startTime), format_time(endTime)].join("/")
end
get_call("#{resource_path}.json")
end

Related

How to improve Ruby structure for Shopify Script Performance

I'm using a Ruby in Shopify Scripts Editor to manage as a security measure Gift With Purchase (GWP) promotions.
The script current is:
Checking if the Customer is logged in as a Professional or Unlogged
Checking if there is a minimum amount spent in the cart
Ensuring that only one "Gift" product is been added to the cart
Removing a "Gift" product if the checkout doesn't have a "Discount Code" or the minimum set in the GWP_SETTINGS = [] obj.
The problem is that it's generating too much Production Errors like "Your script exceeded the time limit." and "Your script exceeded the cpu limit."
The current usage is CPU: 5% | Memory: 8% and it's increasing dizzyingly every time we add a new GWP promotion array.
Is there a better way to structure this logic so it takes less memory to process the entire order + GWP validation?
Here is the "Line Items" structure:
cart = Input.cart
PRO_TAG = 'professional-tag'
has_pro_tag = cart.customer && cart.customer.tags.include?(PRO_TAG)
GWP_SETTINGS = [
gwp_1 = {
"variant_id" => 98989898989898,
"discount_code" => "DISCOUNT_CODE_1",
"minimum_requirement" => Money.new(cents: 50 * 100),
"user_type" => "consumer"
},
gwp_2 = {
"variant_id" => 97979797979797,
"discount_code" => "DISCOUNT_CODE_1",
"minimum_requirement" => Money.new(cents: 50 * 100),
"user_type" => "consumer"
},
gwp_3 = {
"variant_id" => 96969696969696,
"discount_code" => "DISCOUNT_CODE_1",
"minimum_requirement" => Money.new(cents: 50 * 100),
"user_type" => "consumer"
}
]
def remove_GWP(cart, variant_id)
cart.line_items.each do |item|
next if item.variant.id != variant_id
index = cart.line_items.find_index(item)
cart.line_items.delete_at(index)
end
end
def ensure_only_one_GWP_is_added(cart, variant_id)
cart.line_items.each do |item|
next if item.variant.id != variant_id
item.instance_variable_set(:#quantity, 1)
end
end
GWP_SETTINGS.each do |gwp_item_settings|
customer_has_discount = cart.discount_code && cart.discount_code.code == gwp_item_settings["discount_code"]
customer_has_minimum = cart.subtotal_price >= gwp_item_settings["minimum_requirement"]
gwp_is_for_professional = gwp_item_settings["user_type"] == "professional-tag"
#UNLOGGED
if customer_has_discount && customer_has_minimum
ensure_only_one_GWP_is_added(cart, gwp_item_settings["variant_id"])
else
remove_GWP(cart, gwp_item_settings["variant_id"])
end
#PRO
if gwp_is_for_professional && has_pro_tag
if customer_has_discount && customer_has_minimum
ensure_only_one_GWP_is_added(cart, gwp_item_settings["variant_id"])
else
remove_GWP(cart, gwp_item_settings["variant_id"])
end
end
end
Output.cart = cart
You only have 3 settings. But a customer (an order) could have 100+ line items. You know there is only ever 1 customer, 1 order and for you, 3 GWT settings to use.
Your business logic would be smarter if you looped through the line items only once. Then you have a "this is as fast as I can go, go to town" in terms of your algorithm. You cannot go faster than that.
With things like, "does this customer have an X or Y?", you do those once, not 3 times per line item!
As you check each line item, you can do your special logic for things that might AFFECT that line item.
Basically, this is basic algorithmics. You are doing the most work possible repetitively for no reason, and Shopify is puking because of it.

Can GA4-API fetch the data from requests made with a combination of minute and region and sessions?

Problem
With UA, I was able to get the number of sessions per region per minute (a combination of minute, region, and sessions), but is this not possible with GA4?
If not, is there any plan to support this in the future?
Detail
I ran GA4 Query Explorer with date, hour, minute, region in Dimensions and sessions in Metrics.
But I got an incompatibility error.
What I tried
I have checked with GA4 Dimensions & Metrics Explorer and confirmed that the combination of minute and region is not possible. (see image below).
(updated 2022/05/16 15:35)Checked by Code Execution
I ran it with ruby.
require "google/analytics/data/v1beta/analytics_data"
require 'pp'
require 'json'
ENV['GOOGLE_APPLICATION_CREDENTIALS'] = '' # service acount file path
client = ::Google::Analytics::Data::V1beta::AnalyticsData::Client.new
LIMIT_SIZE = 1000
offset = 0
loop do
request = Google::Analytics::Data::V1beta::RunReportRequest.new(
property: "properties/xxxxxxxxx",
date_ranges: [
{ start_date: '2022-04-01', end_date: '2022-04-30'}
],
dimensions: %w(date hour minute region).map { |d| { name: d } },
metrics: %w(sessions).map { |m| { name: m } },
keep_empty_rows: false,
offset: offset,
limit: LIMIT_SIZE
)
ret = client.run_report(request)
dimension_headers = ret.dimension_headers.map(&:name)
metric_headers = ret.metric_headers.map(&:name)
puts (dimension_headers + metric_headers).join(',')
ret.rows.each do |row|
puts (row.dimension_values.map(&:value) + row.metric_values.map(&:value)).join(',')
end
offset += LIMIT_SIZE
break if ret.row_count <= offset
end
The result was an error.
3:The dimensions and metrics are incompatible.. debug_error_string:{"created":"#1652681913.393028000","description":"Error received from peer ipv4:172.217.175.234:443","file":"src/core/lib/surface/call.cc","file_line":953,"grpc_message":"The dimensions and metrics are incompatible.","grpc_status":3}
Error in your code, Make sure you use the actual dimension name and not the UI name. The correct name of that dimension is dateHourMinute not Date hour and minute
dimensions: %w(dateHourMinute).map { |d| { name: d } },
The query explore returns this request just fine
results
Limited use for region dimension
The as for region. As the error message states the dimensions and metrics are incompatible. The issue being that dateHourMinute can not be used with region. Switch to date or datehour
at the time of writing this is a beta api. I have sent a message off to google to find out if this is working as intended or if it may be changed.

RUBY - Currency Exchange Rate Calculator

I'm trying to create a currency converter in Ruby which will calculate the exchange rate between two currencies on a given date.
I have a data file containing test data (date, currency from, currency to). The test data is in EUR, so all rates are converted to EUR and then to the target currency.
So far I have 3 files (Exchange.rb, Test_Exchange.rb, rates.json):
Exchange.rb:
require 'json'
require 'date'
module Exchange
# Return the exchange rate between from_currency and to_currency on date as a float.
# Raises an exception if unable to calculate requested rate.
# Raises an exception if there is no rate for the date provided.
#rates = JSON.parse(File.read('rates.json'))
def self.rate(date, from_currency, to_currency)
# TODO: calculate and return rate
rates = u/rates[date] # get rates of given day
from_to_eur = 1.0 / rates[from_currency] # convert to EUR
from_to_eur * rates[to_currency] # convert to target currency
end
end
Test_Exchange.rb:
require_relative 'Exchange.rb'
require 'date'
target_date = Date.new(2018,12,10).to_s
puts "USD to GBP: #{Exchange.rate(target_date, 'USD', 'GBP')}"
puts "USD to JPY: #{Exchange.rate(target_date, 'PLN', 'CHF')}"
puts "DKK to CAD: #{Exchange.rate(target_date, 'PLN', 'CHF')}"
rates.json:
{
"2018-12-11": {
"USD": 1.1379,
"JPY": 128.75,
"BGN": 1.9558,
"CZK": 25.845,
"DKK": 7.4641,
"GBP": 0.90228,
"HUF": 323.4,
"CHF": 1.1248,
"PLN": 4.2983
},
"2018-12-10": {
"USD": 1.1425,
"JPY": 128.79,
"BGN": 1.9558,
"CZK": 25.866,
"DKK": 7.4639,
"CAD": 1.5218,
"GBP": 0.90245,
"HUF": 323.15,
"PLN": 4.2921,
"CHF": 1.1295,
"ISK": 140.0,
"HRK": 7.387,
"RUB": 75.8985
},
"2018-12-05": {
"USD": 1.1354,
"JPY": 128.31,
"BGN": 1.9558,
"CZK": 25.886,
"DKK": 7.463,
"GBP": 0.88885,
"HUF": 323.49,
"PLN": 4.2826,
"RON": 4.6528,
"SEK": 10.1753,
"CHF": 1.1328,
"HRK": 7.399,
"RUB": 75.8385,
"CAD": 1.5076
}
}
I'm not sure what to add in the Exchange.rb file to allow the user to input a date and the two currencies to compare exchange rates.
Running Exchange.rb does nothing. I'm guessing it wants a date and currency parameters input?
Running Test_Exchange.rb works because the date and currencies are bootstrapped in.
I found almost the same question posted here a couple years ago, but the thread is now closed, and the solution was incomplete. Hoping someone can help me!
RUNNING EDIT:
Exchange.rb:
require 'json'
require 'date'
module Exchange
# Return the exchange rate between from_currency and to_currency on date as a float.
# Raises an exception if unable to calculate requested rate.
# Raises an exception if there is no rate for the date provided.
#rates = JSON.parse(File.read('rates.json'))
#Grab Date and Currencies from User
puts "Please enter a Date (YYYY-MM-DD)"
input_date = gets.chomp
puts "The Date you entered is: #{input_date}"
puts "Please enter a 3-letter Currency Code (ABC):"
input_curr_1 = gets.chomp
puts "The 1st Currency you entered is: #{input_curr_1}"
puts "Please enter a 2nd 3-letter Currency Code (XYZ):"
input_curr_2 = gets.chomp
puts "The 2nd Currency you entered is: #{input_curr_2}"
def self.rate(input_date, input_curr_1, input_curr_2)
# TODO: calculate and return rate
rates = #rates[input_date] # get rates of given day
from_to_eur = 1.0 / rates[input_curr_1] # convert to EUR
from_to_eur * rates[input_curr_2] # convert to target currency
end
end
I think I have to use a put and a get to capture the user date input? Then the same for each of the two currencies? Of course, my syntax for the date stuff is all wrong..
So I managed to use the gets and put functions. Now all that's left is somehow calling the rates.json file and comparing the user inputs to the existing data..

how to exclude from counting holidays in rails4

I am trying to make an app for vacation requests but i am facing a problem.I have e model called VacationRequest and a view of VacationRequest where the result will be shown.
VacationRequest.rb model
def skip_holidays
count1 = 0
special_days = Date.parse("2017-05-09", "2017-05-12")
special_days.each do |sd|
if ((start_data..end_data) === sd)
numero = (end_data - start_data).to_i
numro1 = numero - sd
else
numero = (end_data - start_data).to_i
end
end
end
VacationRequest show.htm.erb
here i called the method from model
#vacation_request.skip_holidays
this is showing errors and is not working. Please help me with that!
My approach to this would be the following:
def skip_holidays
special_days = ["2017-05-09", "2017-05-12"].map(&:to_date)
accepted_days = []
refused_days = []
(start_data..end_data).each do |requested_date|
accepted_days << requested_date unless special_days.include?(requested_date)
refused_days << requested_date if special_days.include?(requested_date)
end
accepted_day_count = accepted_days.count
refused_days_count = refused_days.count
end
This way you iterated over all requested dates (the range), checked if the date is a special day - If so, refuse it, otherwise accept it.
In the end you can display statistics about accepted and refused dates.
You cannot create a date range by passing multiple argument to the constructor. Instead call the constructor twice to create a range:
special_days = Date.parse("2017-05-09") .. Date.parse("2017-05-12")
Or if instead you want only the two dates, do:
special_days = ["2017-05-09", "2017-05-12"].map &:to_date
This Date.parse("2017-05-09", "2017-05-12") don`t create a range, only return the first params parsed:
#irb
Date.parse("2017-05-09", "2017-05-12")
=> Tue, 09 May 2017
You can do it this way:
initial = Date.parse("2017-05-09")
final = Date.parse("2017-05-12")
((initial)..final).each do |date|
#rules here.
end

How calculate the number prorated day with Stripe API?

i am using Stripe. I would like to know how can calculate number of day prorated
I want display something like that
1 additional seat ($9/month each - prorated for 26 days)
in the api i don't see any item prorate_day
Bolo
subscription_proration_date what you are looking for? Then it will calculate it for you.
See more at https://stripe.com/docs/subscriptions/guide
The example of pro-rated subscription in ruby is as follows
# Set your secret key: remember to change this to your live secret key in production
# See your keys here https://dashboard.stripe.com/account/apikeys
Stripe.api_key = "sk_test_9OkpsFpKa1HDHaZa7e0BeGaO"
proration_date = Time.now.to_i
invoice = Stripe::Invoice.upcoming(:customer => "cus_3R1W8PG2DmsmM9", :subscription => "sub_3R3PlB2YlJe84a",
:subscription_plan => "premium_monthly", :subscription_proration_date => proration_date)
current_prorations = invoice.lines.data.select { |ii| ii.period.start == proration_date }
cost = 0
current_prorations.each do |p|
cost += p.amount
end
# Display the cost of these prorations invoice items to the end user,
# and actually do the update when they agree.
# To make sure that the proration is calculated the same as when it was previewed,
# you need to pass in the proration_date parameter
# later...
subscription = Stripe::Subscription.retrieve("sub_3R3PlB2YlJe84a")
subscription.plan = "premium_monthly"
subscription.proration_date = proration_date
subscription.save

Resources