diff --git a/Gemfile.lock b/Gemfile.lock index 176fc46e..780428a9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,7 +99,7 @@ GEM bcrypt (3.1.20) benchmark (0.4.0) bigdecimal (3.1.9) - bootsnap (1.18.4) + bootsnap (1.18.6) msgpack (~> 1.2) brakeman (7.0.2) racc @@ -144,7 +144,7 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - diff-lcs (1.5.1) + diff-lcs (1.6.2) docile (1.4.1) dotenv (3.1.7) dotenv-rails (3.1.7) @@ -174,8 +174,8 @@ GEM gpx (1.2.0) nokogiri (~> 1.7) rake - groupdate (6.5.1) - activesupport (>= 7) + groupdate (6.6.0) + activesupport (>= 7.1) hashdiff (1.1.2) httparty (0.23.1) csv @@ -232,7 +232,7 @@ GEM mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (5.25.5) - msgpack (1.7.3) + msgpack (1.8.0) multi_json (1.15.0) multi_xml (0.7.1) bigdecimal (~> 3.1) @@ -259,7 +259,7 @@ GEM racc (~> 1.4) nokogiri (1.18.8-x86_64-linux-gnu) racc (~> 1.4) - oj (3.16.9) + oj (3.16.10) bigdecimal (>= 3.0) ostruct (>= 0.2) optimist (3.2.0) @@ -360,21 +360,21 @@ GEM rgeo (>= 1.0.0) rspec-core (3.13.3) rspec-support (~> 3.13.0) - rspec-expectations (3.13.3) + rspec-expectations (3.13.4) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.2) + rspec-mocks (3.13.4) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (7.1.1) - actionpack (>= 7.0) - activesupport (>= 7.0) - railties (>= 7.0) + rspec-rails (8.0.0) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) rspec-core (~> 3.13) rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-support (3.13.2) + rspec-support (3.13.3) rswag-api (2.16.0) activesupport (>= 5.2, < 8.1) railties (>= 5.2, < 8.1) @@ -415,10 +415,10 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - sentry-rails (5.23.0) + sentry-rails (5.24.0) railties (>= 5.0) - sentry-ruby (~> 5.23.0) - sentry-ruby (5.23.0) + sentry-ruby (~> 5.24.0) + sentry-ruby (5.24.0) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) shoulda-matchers (6.5.0) diff --git a/app/services/countries_and_cities.rb b/app/services/countries_and_cities.rb index 0785107a..7a260256 100644 --- a/app/services/countries_and_cities.rb +++ b/app/services/countries_and_cities.rb @@ -12,43 +12,64 @@ class CountriesAndCities points .reject { |point| point.country.nil? || point.city.nil? } .group_by(&:country) - .transform_values { |country_points| process_country_points(country_points) } - .map { |country, cities| CountryData.new(country: country, cities: cities) } + .map do |country, country_points| + cities = process_country_points(country_points) + CountryData.new(country: country, cities: cities) if cities.any? + end.compact end private attr_reader :points +# Step 1: Process points to group by consecutive cities and time + def group_points_with_consecutive_cities(country_points) + sorted_points = country_points.sort_by(&:timestamp) + + sessions = [] + current_session = [] + + sorted_points.each_with_index do |point, index| + if current_session.empty? + current_session << point + next + end + + prev_point = sorted_points[index - 1] + + # Split session if city changes or time gap exceeds the threshold + if point.city != prev_point.city + sessions << current_session + current_session = [] + end + + current_session << point + end + + sessions << current_session unless current_session.empty? + sessions + end + + # Step 2: Filter sessions that don't meet the minimum minutes per city + def filter_sessions(sessions) + sessions.map do |session| + end_time = session.last.timestamp + duration = (end_time - session.first.timestamp) / 60 # Convert seconds to minutes + + if duration >= MIN_MINUTES_SPENT_IN_CITY + CityData.new( + city: session.first.city, + points: session.size, + timestamp: end_time, + stayed_for: duration + ) + end + end.compact + end + + # Process points for each country def process_country_points(country_points) - country_points - .group_by(&:city) - .transform_values { |city_points| create_city_data_if_valid(city_points) } - .values - .compact - end - - def create_city_data_if_valid(city_points) - timestamps = city_points.pluck(:timestamp) - duration = calculate_duration_in_minutes(timestamps) - city = city_points.first.city - points_count = city_points.size - - build_city_data(city, points_count, timestamps, duration) - end - - def build_city_data(city, points_count, timestamps, duration) - return nil if duration < ::MIN_MINUTES_SPENT_IN_CITY - - CityData.new( - city: city, - points: points_count, - timestamp: timestamps.max, - stayed_for: duration - ) - end - - def calculate_duration_in_minutes(timestamps) - ((timestamps.max - timestamps.min).to_i / 60) + sessions = group_points_with_consecutive_cities(country_points) + filter_sessions(sessions) end end diff --git a/spec/services/countries_and_cities_spec.rb b/spec/services/countries_and_cities_spec.rb index 636823e5..07e8999e 100644 --- a/spec/services/countries_and_cities_spec.rb +++ b/spec/services/countries_and_cities_spec.rb @@ -6,24 +6,29 @@ RSpec.describe CountriesAndCities do describe '#call' do subject(:countries_and_cities) { described_class.new(points).call } - # we have 5 points in the same city and country within 1 hour, - # 5 points in the differnt city within 10 minutes - # and we expect to get one country with one city which has 5 points + # we have 15 points in the same city and different country within 2 hour, + # 4 points in the differnt city within 10 minutes splitting the country + # and we expect to get one country with one city which has 8 points let(:timestamp) { DateTime.new(2021, 1, 1, 0, 0, 0) } let(:points) do [ - create(:point, city: 'Berlin', country: 'Germany', timestamp:), - create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 10.minutes), - create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 20.minutes), - create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 30.minutes), - create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 40.minutes), - create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 50.minutes), - create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 60.minutes), - create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 70.minutes), - create(:point, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 80.minutes), - create(:point, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 90.minutes) + create(:point, city: 'Kerpen', country: 'Belgium', timestamp:), + create(:point, city: 'Kerpen', country: 'Belgium', timestamp: timestamp + 10.minutes), + create(:point, city: 'Kerpen', country: 'Belgium', timestamp: timestamp + 20.minutes), + create(:point, city: 'Kerpen', country: 'Germany', timestamp: timestamp + 30.minutes), + create(:point, city: 'Kerpen', country: 'Germany', timestamp: timestamp + 40.minutes), + create(:point, city: 'Kerpen', country: 'Germany', timestamp: timestamp + 50.minutes), + create(:point, city: 'Kerpen', country: 'Germany', timestamp: timestamp + 60.minutes), + create(:point, city: 'Kerpen', country: 'Belgium', timestamp: timestamp + 70.minutes), + create(:point, city: 'Kerpen', country: 'Belgium', timestamp: timestamp + 80.minutes), + create(:point, city: 'Kerpen', country: 'Belgium', timestamp: timestamp + 90.minutes), + create(:point, city: 'Kerpen', country: 'Belgium', timestamp: timestamp + 100.minutes), + create(:point, city: 'Kerpen', country: 'Belgium', timestamp: timestamp + 110.minutes), + create(:point, city: 'Kerpen', country: 'Belgium', timestamp: timestamp + 120.minutes), + create(:point, city: 'Kerpen', country: 'Belgium', timestamp: timestamp + 130.minutes), + create(:point, city: 'Kerpen', country: 'Belgium', timestamp: timestamp + 140.minutes) ] end @@ -37,16 +42,12 @@ RSpec.describe CountriesAndCities do expect(countries_and_cities).to eq( [ CountriesAndCities::CountryData.new( - country: 'Germany', + country: 'Belgium', cities: [ CountriesAndCities::CityData.new( - city: 'Berlin', points: 8, timestamp: 1_609_463_400, stayed_for: 70 + city: 'Kerpen', points: 8, timestamp: 1_609_467_600, stayed_for: 70 ) ] - ), - CountriesAndCities::CountryData.new( - country: 'Belgium', - cities: [] ) ] ) @@ -60,21 +61,15 @@ RSpec.describe CountriesAndCities do create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 10.minutes), create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 20.minutes), create(:point, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 80.minutes), - create(:point, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 90.minutes) + create(:point, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 90.minutes), + create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 100.minutes), + create(:point, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 110.minutes) ] end it 'returns countries and cities' do expect(countries_and_cities).to eq( [ - CountriesAndCities::CountryData.new( - country: 'Germany', - cities: [] - ), - CountriesAndCities::CountryData.new( - country: 'Belgium', - cities: [] - ) ] ) end