dawarich/spec/services/location_search/result_aggregator_spec.rb
2025-09-02 21:55:47 +02:00

307 lines
No EOL
12 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
RSpec.describe LocationSearch::ResultAggregator do
let(:service) { described_class.new }
describe '#group_points_into_visits' do
context 'with empty points array' do
it 'returns empty array' do
result = service.group_points_into_visits([])
expect(result).to eq([])
end
end
context 'with single point' do
let(:single_point) do
{
id: 1,
timestamp: 1711814700,
coordinates: [52.5200, 13.4050],
distance_meters: 45.5,
accuracy: 10,
date: '2024-03-20T18:45:00Z',
city: 'Berlin',
country: 'Germany',
altitude: 100
}
end
it 'creates a single visit' do
result = service.group_points_into_visits([single_point])
expect(result.length).to eq(1)
visit = result.first
expect(visit[:timestamp]).to eq(1711814700)
expect(visit[:coordinates]).to eq([52.5200, 13.4050])
expect(visit[:points_count]).to eq(1)
end
it 'estimates duration for single point visits' do
result = service.group_points_into_visits([single_point])
visit = result.first
expect(visit[:duration_estimate]).to eq('~15 minutes')
expect(visit[:visit_details][:duration_minutes]).to eq(15)
end
end
context 'with consecutive points' do
let(:consecutive_points) do
[
{
id: 1,
timestamp: 1711814700, # 18:45
coordinates: [52.5200, 13.4050],
distance_meters: 45.5,
accuracy: 10,
date: '2024-03-20T18:45:00Z',
city: 'Berlin',
country: 'Germany'
},
{
id: 2,
timestamp: 1711816500, # 19:15 (30 minutes later)
coordinates: [52.5201, 13.4051],
distance_meters: 48.2,
accuracy: 8,
date: '2024-03-20T19:15:00Z',
city: 'Berlin',
country: 'Germany'
},
{
id: 3,
timestamp: 1711817400, # 19:30 (15 minutes later)
coordinates: [52.5199, 13.4049],
distance_meters: 42.1,
accuracy: 12,
date: '2024-03-20T19:30:00Z',
city: 'Berlin',
country: 'Germany'
}
]
end
it 'groups consecutive points into single visit' do
result = service.group_points_into_visits(consecutive_points)
expect(result.length).to eq(1)
visit = result.first
expect(visit[:points_count]).to eq(3)
end
it 'calculates visit duration from start to end' do
result = service.group_points_into_visits(consecutive_points)
visit = result.first
expect(visit[:duration_estimate]).to eq('~45 minutes')
expect(visit[:visit_details][:duration_minutes]).to eq(45)
end
it 'uses most accurate point coordinates' do
result = service.group_points_into_visits(consecutive_points)
visit = result.first
# Point with accuracy 8 should be selected
expect(visit[:coordinates]).to eq([52.5201, 13.4051])
expect(visit[:accuracy_meters]).to eq(8)
end
it 'calculates average distance' do
result = service.group_points_into_visits(consecutive_points)
visit = result.first
expected_avg = (45.5 + 48.2 + 42.1) / 3
expect(visit[:distance_meters]).to eq(expected_avg.round(2))
end
it 'sets correct start and end times' do
result = service.group_points_into_visits(consecutive_points)
visit = result.first
expect(visit[:visit_details][:start_time]).to eq('2024-03-20T18:45:00Z')
expect(visit[:visit_details][:end_time]).to eq('2024-03-20T19:30:00Z')
end
end
context 'with separate visits (time gaps)' do
let(:separate_visits_points) do
[
{
id: 1,
timestamp: 1711814700, # 18:45
coordinates: [52.5200, 13.4050],
distance_meters: 45.5,
accuracy: 10,
date: '2024-03-20T18:45:00Z',
city: 'Berlin',
country: 'Germany'
},
{
id: 2,
timestamp: 1711816500, # 19:15 (30 minutes later - within threshold)
coordinates: [52.5201, 13.4051],
distance_meters: 48.2,
accuracy: 8,
date: '2024-03-20T19:15:00Z',
city: 'Berlin',
country: 'Germany'
},
{
id: 3,
timestamp: 1711820100, # 20:15 (60 minutes after last point - exceeds threshold)
coordinates: [52.5199, 13.4049],
distance_meters: 42.1,
accuracy: 12,
date: '2024-03-20T20:15:00Z',
city: 'Berlin',
country: 'Germany'
}
]
end
it 'creates separate visits when time gap exceeds threshold' do
result = service.group_points_into_visits(separate_visits_points)
expect(result.length).to eq(2)
expect(result.first[:points_count]).to eq(1) # Most recent visit (20:15)
expect(result.last[:points_count]).to eq(2) # Earlier visit (18:45-19:15)
end
it 'orders visits by timestamp descending (most recent first)' do
result = service.group_points_into_visits(separate_visits_points)
expect(result.first[:timestamp]).to be > result.last[:timestamp]
end
end
context 'with duration formatting' do
let(:points_with_various_durations) do
# Helper to create points with time differences
base_time = 1711814700
[
# Short visit (25 minutes) - 2 points 25 minutes apart
{ id: 1, timestamp: base_time, accuracy: 10, coordinates: [52.5200, 13.4050], distance_meters: 50, date: '2024-03-20T18:45:00Z' },
{ id: 2, timestamp: base_time + 25 * 60, accuracy: 10, coordinates: [52.5200, 13.4050], distance_meters: 50, date: '2024-03-20T19:10:00Z' },
# Long visit (2 hours 15 minutes) - points every 15 minutes to stay within 30min threshold
{ id: 3, timestamp: base_time + 70 * 60, accuracy: 10, coordinates: [52.5300, 13.4100], distance_meters: 30, date: '2024-03-20T19:55:00Z' },
{ id: 4, timestamp: base_time + 85 * 60, accuracy: 10, coordinates: [52.5300, 13.4100], distance_meters: 30, date: '2024-03-20T20:10:00Z' },
{ id: 5, timestamp: base_time + 100 * 60, accuracy: 10, coordinates: [52.5300, 13.4100], distance_meters: 30, date: '2024-03-20T20:25:00Z' },
{ id: 6, timestamp: base_time + 115 * 60, accuracy: 10, coordinates: [52.5300, 13.4100], distance_meters: 30, date: '2024-03-20T20:40:00Z' },
{ id: 7, timestamp: base_time + 130 * 60, accuracy: 10, coordinates: [52.5300, 13.4100], distance_meters: 30, date: '2024-03-20T20:55:00Z' },
{ id: 8, timestamp: base_time + 145 * 60, accuracy: 10, coordinates: [52.5300, 13.4100], distance_meters: 30, date: '2024-03-20T21:10:00Z' },
{ id: 9, timestamp: base_time + 160 * 60, accuracy: 10, coordinates: [52.5300, 13.4100], distance_meters: 30, date: '2024-03-20T21:25:00Z' },
{ id: 10, timestamp: base_time + 175 * 60, accuracy: 10, coordinates: [52.5300, 13.4100], distance_meters: 30, date: '2024-03-20T21:40:00Z' },
{ id: 11, timestamp: base_time + 190 * 60, accuracy: 10, coordinates: [52.5300, 13.4100], distance_meters: 30, date: '2024-03-20T21:55:00Z' },
{ id: 12, timestamp: base_time + 205 * 60, accuracy: 10, coordinates: [52.5300, 13.4100], distance_meters: 30, date: '2024-03-20T22:10:00Z' }
]
end
it 'formats duration correctly for minutes only' do
short_visit_points = points_with_various_durations.take(2)
result = service.group_points_into_visits(short_visit_points)
expect(result.first[:duration_estimate]).to eq('~25 minutes')
end
it 'formats duration correctly for hours and minutes' do
long_visit_points = points_with_various_durations.drop(2)
result = service.group_points_into_visits(long_visit_points)
expect(result.first[:duration_estimate]).to eq('~2 hours 15 minutes')
end
it 'formats duration correctly for hours only' do
# Create points within threshold but exactly 2 hours apart from first to last
exact_hour_points = [
{ id: 1, timestamp: 1711814700, accuracy: 10, coordinates: [52.5200, 13.4050], distance_meters: 50, date: '2024-03-20T18:45:00Z' },
{ id: 2, timestamp: 1711814700 + 25 * 60, accuracy: 10, coordinates: [52.5200, 13.4050], distance_meters: 50, date: '2024-03-20T19:10:00Z' },
{ id: 3, timestamp: 1711814700 + 50 * 60, accuracy: 10, coordinates: [52.5200, 13.4050], distance_meters: 50, date: '2024-03-20T19:35:00Z' },
{ id: 4, timestamp: 1711814700 + 75 * 60, accuracy: 10, coordinates: [52.5200, 13.4050], distance_meters: 50, date: '2024-03-20T20:00:00Z' },
{ id: 5, timestamp: 1711814700 + 100 * 60, accuracy: 10, coordinates: [52.5200, 13.4050], distance_meters: 50, date: '2024-03-20T20:25:00Z' },
{ id: 6, timestamp: 1711814700 + 120 * 60, accuracy: 10, coordinates: [52.5200, 13.4050], distance_meters: 50, date: '2024-03-20T20:45:00Z' }
]
result = service.group_points_into_visits(exact_hour_points)
expect(result.first[:duration_estimate]).to eq('~2 hours')
end
end
context 'with altitude data' do
let(:points_with_altitude) do
[
{
id: 1, timestamp: 1711814700, coordinates: [52.5200, 13.4050],
accuracy: 10, distance_meters: 50, altitude: 100,
date: '2024-03-20T18:45:00Z'
},
{
id: 2, timestamp: 1711815600, coordinates: [52.5201, 13.4051],
accuracy: 10, distance_meters: 50, altitude: 105,
date: '2024-03-20T19:00:00Z'
},
{
id: 3, timestamp: 1711816500, coordinates: [52.5199, 13.4049],
accuracy: 10, distance_meters: 50, altitude: 95,
date: '2024-03-20T19:15:00Z'
}
]
end
it 'includes altitude range in visit details' do
result = service.group_points_into_visits(points_with_altitude)
visit = result.first
expect(visit[:visit_details][:altitude_range]).to eq('95m - 105m')
end
context 'with same altitude for all points' do
before do
points_with_altitude.each { |p| p[:altitude] = 100 }
end
it 'shows single altitude value' do
result = service.group_points_into_visits(points_with_altitude)
visit = result.first
expect(visit[:visit_details][:altitude_range]).to eq('100m')
end
end
context 'with missing altitude data' do
before do
points_with_altitude.each { |p| p.delete(:altitude) }
end
it 'handles missing altitude gracefully' do
result = service.group_points_into_visits(points_with_altitude)
visit = result.first
expect(visit[:visit_details][:altitude_range]).to be_nil
end
end
end
context 'with unordered points' do
let(:unordered_points) do
[
{ id: 3, timestamp: 1711817400, coordinates: [52.5199, 13.4049], accuracy: 10, distance_meters: 50, date: '2024-03-20T19:30:00Z' },
{ id: 1, timestamp: 1711814700, coordinates: [52.5200, 13.4050], accuracy: 10, distance_meters: 50, date: '2024-03-20T18:45:00Z' },
{ id: 2, timestamp: 1711816500, coordinates: [52.5201, 13.4051], accuracy: 10, distance_meters: 50, date: '2024-03-20T19:15:00Z' }
]
end
it 'handles unordered input correctly' do
result = service.group_points_into_visits(unordered_points)
visit = result.first
expect(visit[:visit_details][:start_time]).to eq('2024-03-20T18:45:00Z')
expect(visit[:visit_details][:end_time]).to eq('2024-03-20T19:30:00Z')
end
end
end
end