# frozen_string_literal: true class Stats::DailyDistanceQuery def initialize(monthly_points, timespan, timezone = nil) @monthly_points = monthly_points @timespan = timespan @timezone = validate_timezone(timezone) end def call daily_distances = daily_distances(monthly_points) distance_by_day_map = distance_by_day_map(daily_distances) convert_to_daily_distances(distance_by_day_map) end private attr_reader :monthly_points, :timespan, :timezone def daily_distances(monthly_points) Stat.connection.select_all(<<-SQL.squish) WITH points_with_distances AS ( SELECT EXTRACT(day FROM (to_timestamp(timestamp) AT TIME ZONE 'UTC' AT TIME ZONE '#{timezone}')) as day_of_month, CASE WHEN LAG(lonlat) OVER ( PARTITION BY EXTRACT(day FROM (to_timestamp(timestamp) AT TIME ZONE 'UTC' AT TIME ZONE '#{timezone}')) ORDER BY timestamp ) IS NOT NULL THEN ST_Distance( lonlat::geography, LAG(lonlat) OVER ( PARTITION BY EXTRACT(day FROM (to_timestamp(timestamp) AT TIME ZONE 'UTC' AT TIME ZONE '#{timezone}')) ORDER BY timestamp )::geography ) ELSE 0 END as segment_distance FROM (#{monthly_points.to_sql}) as points ) SELECT day_of_month, ROUND(COALESCE(SUM(segment_distance), 0)) as distance_meters FROM points_with_distances GROUP BY day_of_month ORDER BY day_of_month SQL end def distance_by_day_map(daily_distances) daily_distances.index_by do |row| row['day_of_month'].to_i end end def convert_to_daily_distances(distance_by_day_map) timespan.to_a.map.with_index(1) do |day, index| distance_meters = distance_by_day_map[day.day]&.fetch('distance_meters', 0) || 0 [index, distance_meters.to_i] end end def validate_timezone(timezone) return timezone if ActiveSupport::TimeZone.all.any? { |tz| tz.name == timezone } 'UTC' end end