Skip to content
Permalink
Browse files

Add dailyFlightStats

Generate a on-time stats by day to generate charts.
  • Loading branch information...
pboyd committed Oct 13, 2019
1 parent 131fc98 commit 4e64f5842b389c078edc19ced17695a4e8b58af0
@@ -120,6 +120,21 @@ func makeSchema(db *sql.DB) (graphql.Schema, error) {
Resolve: resolveFlightStatsByAirline(db),
}

dailyFlightStats := &graphql.Field{
Type: dailyFlightStats,
Args: graphql.FieldConfigArgument{
"origin": &graphql.ArgumentConfig{
Type: graphql.String,
Description: "airport IATA code (e.g. LAX)",
},
"destination": &graphql.ArgumentConfig{
Type: graphql.String,
Description: "airport IATA code (e.g. LAX)",
},
},
Resolve: resolveDailyFlightStats(db),
}

return graphql.NewSchema(
graphql.SchemaConfig{
Query: graphql.NewObject(
@@ -129,6 +144,7 @@ func makeSchema(db *sql.DB) (graphql.Schema, error) {
"airport": airportQuery,
"airportList": airportList,
"flightStatsByAirline": flightStatsByAirline,
"dailyFlightStats": dailyFlightStats,
},
},
),
@@ -89,3 +89,100 @@ func resolveFlightStatsByAirline(db *sql.DB) graphql.FieldResolveFn {
},
)
}

type dailyStatsAirline struct {
Airline string
Days []*flightStatsDay
}

type flightStatsDay struct {
Date time.Time
OnTimePercentage float64
}

var dailyFlightStatsRow = graphql.NewObject(
graphql.ObjectConfig{
Name: "dailyFlightStatsDay",
Fields: graphql.Fields{
"date": &graphql.Field{Type: graphql.DateTime},
"flights": &graphql.Field{Type: graphql.Int},
"delays": &graphql.Field{Type: graphql.Int},
"onTimePercentage": &graphql.Field{Type: graphql.Float},
},
},
)

var dailyFlightStats = graphql.NewList(
graphql.NewObject(
graphql.ObjectConfig{
Name: "dailyFlightStats",
Fields: graphql.Fields{
"airline": &graphql.Field{Type: graphql.String},
"days": &graphql.Field{Type: graphql.NewList(dailyFlightStatsRow)},
},
},
),
)

func resolveDailyFlightStats(db *sql.DB) graphql.FieldResolveFn {
return graphQLMetrics("daily_flight_stats",
func(p graphql.ResolveParams) (interface{}, error) {
origin, _ := p.Args["origin"].(string)
origin = strings.ToUpper(origin)

dest, _ := p.Args["destination"].(string)
dest = strings.ToUpper(dest)

if !isAirportCode(origin) || !isAirportCode(dest) {
return nil, nil
}

rows, err := db.QueryContext(p.Context,
`SELECT
date,
carriers.name,
total_flights,
IF(delayed_flights IS NULL, 0, delayed_flights) AS delay_flights_not_null
FROM
flights_day
INNER JOIN carriers ON carrier=carriers.code
WHERE origin=? AND destination=?
ORDER BY date`,
origin, dest)
if err != nil {
return nil, err
}
defer rows.Close()

statsMap := map[string][]*flightStatsDay{}

for rows.Next() {
var (
airline string
row flightStatsDay
flights, delays int
)

err := rows.Scan(&row.Date, &airline, &flights, &delays)
if err != nil {
return nil, err
}

row.OnTimePercentage = (1.0 - float64(delays)/float64(flights)) * 100

if statsMap[airline] == nil {
statsMap[airline] = []*flightStatsDay{}
}

statsMap[airline] = append(statsMap[airline], &row)
}

stats := make([]dailyStatsAirline, 0, len(statsMap))
for airline, days := range statsMap {
stats = append(stats, dailyStatsAirline{Airline: airline, Days: days})
}

return stats, nil
},
)
}
@@ -7,6 +7,7 @@ import (

type FlightStatsStore interface {
FlightStatsByAirline(ctx context.Context, origin, destination string) ([]*FlightStats, error)
DailyFlightStats(ctx context.Context, origin, destination string) (map[string][]*FlightStatsDay, error)
}

type FlightStats struct {
@@ -17,9 +18,27 @@ type FlightStats struct {
}

func (fs *FlightStats) OnTimePercentage() float64 {
if fs.TotalFlights <= 0 {
return calcOnTimePercentage(fs.TotalFlights, fs.TotalDelays)
}

type FlightStatsDay struct {
Date time.Time
Flights int
Delays int
}

func (fs *FlightStatsDay) OnTimePercentage() float64 {
return calcOnTimePercentage(fs.Flights, fs.Delays)
}

func calcOnTimePercentage(total, delayed int) float64 {
if total <= 0 {
return 0
}

return (1.0 - float64(fs.TotalDelays)/float64(fs.TotalFlights)) * 100
return (1.0 - float64(delayed)/float64(total)) * 100
}

type OnTimeStat interface {
OnTimePercentage() float64
}
@@ -51,10 +51,80 @@ func (p *Processor) resolveFlightStatsByAirlineQuery(params graphql.ResolveParam
}

func resolveOnTimePercentage(params graphql.ResolveParams) (interface{}, error) {
stats, ok := params.Source.(*app.FlightStats)
stats, ok := params.Source.(app.OnTimeStat)
if !ok {
return 0, nil
}

return stats.OnTimePercentage(), nil
}

var dailyFlightStatsRow = graphql.NewObject(
graphql.ObjectConfig{
Name: "dailyFlightStatsDay",
Fields: graphql.Fields{
"date": &graphql.Field{Type: graphql.DateTime},
"flights": &graphql.Field{Type: graphql.Int},
"delays": &graphql.Field{Type: graphql.Int},
"onTimePercentage": &graphql.Field{Type: graphql.Float, Resolve: resolveOnTimePercentage},
},
},
)

var dailyFlightStats = graphql.NewList(
graphql.NewObject(
graphql.ObjectConfig{
Name: "dailyFlightStats",
Fields: graphql.Fields{
"airline": &graphql.Field{Type: graphql.String},
"days": &graphql.Field{Type: graphql.NewList(dailyFlightStatsRow)},
},
},
),
)

func (p *Processor) dailyFlightStatsQuery() *graphql.Field {
return &graphql.Field{
Type: dailyFlightStats,
Args: graphql.FieldConfigArgument{
"origin": &graphql.ArgumentConfig{
Type: graphql.String,
Description: "airport IATA code (e.g. LAX)",
},
"destination": &graphql.ArgumentConfig{
Type: graphql.String,
Description: "airport IATA code (e.g. LAX)",
},
},
Resolve: instrumentResolver("daily_flight_stats", p.resolveDailyFlightStats),
}
}

type dailyStatsAirline struct {
Airline string
Days []*app.FlightStatsDay
}

func (p *Processor) resolveDailyFlightStats(params graphql.ResolveParams) (interface{}, error) {
origin, _ := params.Args["origin"].(string)
origin = strings.ToUpper(origin)

dest, _ := params.Args["destination"].(string)
dest = strings.ToUpper(dest)

if !app.IsAirportCode(origin) || !app.IsAirportCode(dest) {
return nil, nil
}

statsMap, err := p.config.FlightStatsStore.DailyFlightStats(params.Context, origin, dest)
if err != nil {
return nil, err
}

stats := make([]dailyStatsAirline, 0, len(statsMap))
for airline, days := range statsMap {
stats = append(stats, dailyStatsAirline{Airline: airline, Days: days})
}

return stats, nil
}
@@ -3,6 +3,7 @@ package graphql
import (
"context"
"testing"
"time"

"github.com/pboyd/flightranker-backend/backendb/app"
)
@@ -42,3 +43,47 @@ func TestFlightStatsByAirline(t *testing.T) {
}
}
}

func TestDailyFlightStats(t *testing.T) {
cases := []struct {
stats map[string][]*app.FlightStatsDay
query string
expected string
}{
{
stats: map[string][]*app.FlightStatsDay{
"Delta": []*app.FlightStatsDay{
{Date: date(2019, 01, 01), Flights: 100, Delays: 10},
{Date: date(2019, 01, 02), Flights: 10, Delays: 0},
{Date: date(2019, 01, 03), Flights: 23, Delays: 4},
},
},
query: `{dailyFlightStats(origin:"SOX",destination:"SAX"){airline,days{date,flights,delays,onTimePercentage}}}`,
expected: `{"dailyFlightStats":[{"airline":"Delta","days":[{"date":"2019-01-01T00:00:00Z","delays":10,"flights":100,"onTimePercentage":90},{"date":"2019-01-02T00:00:00Z","delays":0,"flights":10,"onTimePercentage":100},{"date":"2019-01-03T00:00:00Z","delays":4,"flights":23,"onTimePercentage":82.6086956521739}]}]}`,
},
}

for _, c := range cases {
p := NewProcessor(ProcessorConfig{
FlightStatsStore: &app.FlightStatsStoreMock{
DailyFlightStatsFn: func(ctx context.Context, origin, dest string) (map[string][]*app.FlightStatsDay, error) {
return c.stats, nil
},
},
})

actual, err := p.Do(context.Background(), c.query)
if err != nil {
t.Errorf("got error %v, want nil", err)
continue
}

if actual != c.expected {
t.Errorf("\ngot: %s\nwant: %s", actual, c.expected)
}
}
}

func date(year int, month time.Month, day int) time.Time {
return time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
}
@@ -35,6 +35,7 @@ func NewProcessor(config ProcessorConfig) *Processor {
"airport": processor.airportQuery(),
"airportList": processor.airportListQuery(),
"flightStatsByAirline": processor.flightStatsByAirlineQuery(),
"dailyFlightStats": processor.dailyFlightStatsQuery(),
},
},
),
@@ -59,3 +59,44 @@ func (s *Store) airlineFlightInfo(ctx context.Context, origin, dest string) ([]*

return stats, nil
}

func (s *Store) DailyFlightStats(ctx context.Context, origin, destination string) (map[string][]*app.FlightStatsDay, error) {
rows, err := s.db.QueryContext(ctx,
`SELECT
date,
carriers.name,
total_flights,
IF(delayed_flights IS NULL, 0, delayed_flights) AS delay_flights_not_null
FROM
flights_day
INNER JOIN carriers ON carrier=carriers.code
WHERE origin=? AND destination=?
ORDER BY date`,
origin, destination)
if err != nil {
return nil, err
}
defer rows.Close()

stats := map[string][]*app.FlightStatsDay{}

for rows.Next() {
var (
airline string
row app.FlightStatsDay
)

err := rows.Scan(&row.Date, &airline, &row.Flights, &row.Delays)
if err != nil {
return nil, err
}

if stats[airline] == nil {
stats[airline] = []*app.FlightStatsDay{}
}

stats[airline] = append(stats[airline], &row)
}

return stats, nil
}

0 comments on commit 4e64f58

Please sign in to comment.
You can’t perform that action at this time.