Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ group :development, :test do
gem "brakeman", require: false
gem "rubocop-rails-omakase", require: false

gem 'factory_bot_rails'
gem 'rspec-rails'
end
gem "factory_bot_rails"
gem "rspec-rails"

gem "pry-byebug"
gem "pry-rails"
end
13 changes: 13 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ GEM
brakeman (7.1.0)
racc
builder (3.3.0)
byebug (12.0.0)
coderay (1.1.3)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
crass (1.0.6)
Expand Down Expand Up @@ -139,6 +141,7 @@ GEM
net-pop
net-smtp
marcel (1.0.4)
method_source (1.1.0)
mini_mime (1.1.5)
minitest (5.25.5)
msgpack (1.8.0)
Expand Down Expand Up @@ -186,6 +189,14 @@ GEM
prettyprint
prettyprint (0.2.0)
prism (1.4.0)
pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
pry-byebug (3.11.0)
byebug (~> 12.0)
pry (>= 0.13, < 0.16)
pry-rails (0.3.11)
pry (>= 0.13.0)
psych (5.2.6)
date
stringio
Expand Down Expand Up @@ -347,6 +358,8 @@ DEPENDENCIES
factory_bot_rails
kamal
pg (>= 1.1)
pry-byebug
pry-rails
puma (>= 5.0)
rails (~> 8.0.2, >= 8.0.2.1)
rspec-rails
Expand Down
25 changes: 25 additions & 0 deletions app/controllers/api/v1/flight_searches_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module Api
module V1
class FlightSearchesController < ApplicationController
before_action :check_departure_dates, only: [ :index ]

def index
search_params = params.permit(:carrier, :origin_iata, :destination_iata, :departure_from, :departure_to)

routes = FlightRouteSearchService.call(search_params)

render json: routes, status: (routes.any? ? :ok : :not_found)
end

private

def check_departure_dates
if params[:departure_from].blank? || params[:departure_to].blank?
render json: [], status: :unprocessable_entity
end
end
end
end
end
4 changes: 3 additions & 1 deletion app/models/permitted_route.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: permitted_routes
Expand All @@ -14,4 +16,4 @@

class PermittedRoute < ApplicationRecord
validates :carrier, :origin_iata, :destination_iata, presence: true
end
end
4 changes: 3 additions & 1 deletion app/models/segment.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: segments
Expand All @@ -16,4 +18,4 @@
class Segment < ApplicationRecord
validates :airline, :segment_number, presence: true
validates :origin_iata, :destination_iata, presence: true, length: { is: 3 }
end
end
51 changes: 51 additions & 0 deletions app/services/flight_route/finder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

module FlightRoute
class Finder
def self.call(carrier:, origin:, destination:)
query = <<-SQL.squish
WITH transferpoint_routes AS (
SELECT
pr.id,
pr.origin_iata,
pr.destination_iata,
tp AS transferpoint
FROM permitted_routes pr
CROSS JOIN LATERAL unnest(COALESCE(pr.transfer_iata_codes, ARRAY[]::text[])) AS tp
WHERE pr.origin_iata = '#{origin}' AND pr.destination_iata = '#{destination}' AND pr.carrier = '#{carrier}'#{' '}
),
direct_only AS (
SELECT
pr.id,
pr.origin_iata,
pr.destination_iata,
NULL::text AS transferpoint
FROM permitted_routes pr
WHERE pr.direct = true
AND pr.origin_iata = '#{origin}'
AND pr.destination_iata = '#{destination}'
AND pr.carrier = '#{carrier}'
)
SELECT
id,
origin_iata,
destination_iata,
CASE
WHEN transferpoint IS NULL THEN ARRAY[origin_iata, destination_iata]
ELSE ARRAY[origin_iata, transferpoint, destination_iata]#{' '}
END AS route
FROM (
SELECT * FROM transferpoint_routes
UNION ALL
SELECT * FROM direct_only
) all_routes
ORDER BY id, route;
SQL

ActiveRecord::Base.connection.execute(query).to_a.map do |row|
row["route"] = row["route"].delete("{}").split(",")
row
end
end
end
end
87 changes: 87 additions & 0 deletions app/services/flight_route/validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# frozen_string_literal: true

module FlightRoute
class Validator
MIN_CONNECTION_TIME = 480.minutes # 8 часов
MAX_CONNECTION_TIME = 2880.minutes # 48 часов

class << self
def available_routes(routes, airline:, departure_from:, departure_to:)
segments_map = build_segments_map(routes, airline, departure_from, departure_to)

routes.map do |route_hash|
route = route_hash[:route] || route_hash["route"]
segments_sequence = route_segments(route, segments_map)
next unless segments_sequence

{
origin_iata: segments_sequence.first.origin_iata,
destination_iata: segments_sequence.last.destination_iata,
departure_time: segments_sequence.first.std,
arrival_time: segments_sequence.last.sta,
segments: segments_sequence.map do |s|
{
carrier: s.airline,
segment_number: s.segment_number,
origin_iata: s.origin_iata,
destination_iata: s.destination_iata,
std: s.std,
sta: s.sta
}
end
}
end.compact
end

private

def expand_route(route)
route.flat_map { |point| point.length > 3 ? point.scan(/.{3}/) : [ point ] }
end

def route_segments(route, segments_map)
expanded_route = expand_route(route)
segments_sequence = []

expanded_route.each_cons(2) do |from_iata, to_iata|
possible_segments = segments_map[[ from_iata, to_iata ]] || []
return nil if possible_segments.empty?

if segments_sequence.empty?
segment = possible_segments.first
else
prev = segments_sequence.last
segment = possible_segments.find do |s|
layover = s.std - prev.sta
layover >= MIN_CONNECTION_TIME && layover <= MAX_CONNECTION_TIME
end
end

return nil unless segment
segments_sequence << segment
end

segments_sequence
end

def build_segments_map(routes, airline, departure_from, departure_to)
from = Date.parse(departure_from).beginning_of_day
to = Date.parse(departure_to).end_of_day

pairs = routes.flat_map do |route_hash|
route = route_hash[:route] || route_hash["route"]
expand_route(route).each_cons(2).to_a
end.uniq

segments = Segment.where([ pairs.map { |from, to| "(origin_iata = ? AND destination_iata = ?)" }.join(" OR "), *pairs.flatten ])
.where(airline: airline)
.where(std: from..to)
.order(:std)

segments_map = Hash.new { |h, k| h[k] = [] }
segments.each { |s| segments_map[[ s.origin_iata, s.destination_iata ]] << s }
segments_map
end
end
end
end
31 changes: 31 additions & 0 deletions app/services/flight_route_search_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

class FlightRouteSearchService
def self.call(params)
new(params).call
end

def initialize(params)
@carrier = params[:carrier]
@origin_iata = params[:origin_iata]
@destination_iata = params[:destination_iata]
@departure_from = params[:departure_from]
@departure_to = params[:departure_to]
end

def call
routes = FlightRoute::Finder.call(carrier: @carrier, origin: @origin_iata, destination: @destination_iata)
available_routes = []

if routes.any?
available_routes = FlightRoute::Validator.available_routes(
routes,
airline: @carrier,
departure_from: @departure_from,
departure_to: @departure_to
)
end

available_routes
end
end
6 changes: 6 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@

# Defines the root path route ("/")
# root "posts#index"

namespace :api do
namespace :v1 do
resources :flight_searches, only: [ :index ]
end
end
end
Loading