I really wish it weren't so, because good people there are hurting badly, but all of a sudden, thanks to the global credit crunch, Iceland has become a cheap destination. Icelandair will fly you to Reykjavik from Boston or New York and put you up in the Hilton for three nights. Total cost: $559+ $90 in various airport taxes. If you want an extra night, it's $69.

Given what that wonderful country has to offer, and what the right price for that is, this is an incredibly good deal, so I got busy. Kirstin and I could fly there between December 10 and 14. Got to find flights from Raleigh-Durham to either Boston or NYC, at the right times. That means toggling between Expedia.com and Icelandair.com, ballpen in hand, building and scratching out various itineraries.

All that recomputing of total costs, layovers, etc. with different flight combinations is not much fun. And I like seeing my data in some compact and logical form: like outbound flights arranged A to B to C, and inbound flights C to B to A. I like my layovers spelled out so I can quickly see if they're acceptable, and I like to see the total cost of the whole thing. Finally, I don't want this information scattered across several pages.

This presented an opportunity for yet another frivolous use of Stata:

 
capture prog drop printItinerary
prog def printItinerary

// user input
// ##########

// enter the number of travelers
local people 2

// airport lists
// you may enter either airport codes or city names, but if you choose
// the latter, enter them as one word (e.g. NewYork). if the return trip
// is along the same airport string, enter "same". either airport list
// can be empty or missing, in case you want to plan a one-way trip.

local airports_there "RDU JFK KEF"
local airports_back "same"

// flight itinerary
// enter dates in the format DDmonYYYY
// enter legs of the trip as strings of 4, 6 or 8 words.
// 4 words: airline, flight#, dep_hh:mm.am/pm, arr_hh:mm.am/pm
// 6 words if either the departure or arrival are on next day.
// 8 words if both are. (i.e., "next day" x 2 = 4 words)

local date_there "10dec2008" // as of departure, not arrival
local date_back "14dec2008" // ditto.

local leg1_there "Delta 6742 4:00pm 6:00pm"
local leg2_there "Icelandair 614 8:00pm 6:45am next day"

local leg2_back "Icelandair 615 5:05pm 6:10pm"
local leg1_back "Delta 6227 7:10pm 9:30pm"

// prices
// enter numbers in the order of legs counted
// from destination. on the way back, enter
// zero for legs priced as round-trip.
// for any airport taxes, etc. not
// included in the prices by leg, enter
// the total amount per traveler.

local prices_there "199 559"
local prices_back "199 0"
local otherfees 90

// you're done. now watch.

// calculations
// ############

// set some constants based on layout of input strings
local airln 1 // airline is always 1st word
local flightno 2 // flight # is always 2nd word
local deptime 3 // departure time is always 3rd word
local ways "there back"

// calculate ends and legs
foreach k in `ways' {
local ends_`k': list sizeof airports_`k' // all airports of the trip
local legs_`k'=0
if `ends_`k''>0 {
if `ends_`k''==1 & "`airports_`k''"!="same" {
di as error "You must have at least two airports if you have any."
error 102
}
else if `ends_`k''>1 {
local legs_`k' =`ends_`k''-1 // trip legs
}
}
if "`k'"=="back" & "`airports_`k''"=="same" {
foreach w in ends legs {
local `w'_`k'=``w'_there'
}
local airports_`k' "`airports_there'"
}
}

// initialize some locals you'll need later
foreach k in `ways' {
local layovers_`k'=0
}

// final input check
foreach k in `ways' {
if `legs_`k''>0 {
forvalues i=1/`legs_`k'' {
local leg_`i' "leg`i'_`k'"
local check: list sizeof `leg_`i''
if `check'!=4 & `check'!=6 & `check'!=8 {
di as error "check your flight inputs."
di as error "each should have 4, 6, or 8 words:"
di as error "airline" // 1 word
di as error "flight #" // 1 word
di as error "(departure) hh:mm" // 1 word
di as error "[next day]" // 2 words
di as error "(arrival) hh:mm" // 1 word
di as error "[next day]" // 2 words
error 102
}
}
}
}

// calculate cost
foreach k in `ways' {
local cost_`k'=""
if `legs_`k''>0 {
forvalues i=1/`legs_`k'' {
local thiscost: word `i' of `prices_`k''
local cost_`k' "`cost_`k''`thiscost'+"
}
}
}
local cost=(`cost_there'`cost_back'`otherfees')*`people'

// build itinerary lines and calculate layovers
foreach k in `ways' {
// default layovers: same day
local layovers_`k'=0
if `legs_`k''>0 {
// default arrival date: same as departure date
local `k'_at_`legs_`k'' "`date_`k''"
forvalues i=1/`legs_`k'' {
local from=`i'
local to=`i'+1
local input "`leg`from'_`k''"
local next_leg "`leg`to'_`k''"
if "`k'"=="back" {
local j=`ends_`k''+1-`i'
local from=`j'
local to=`j'-1
local next=`to'-1
local input "`leg`to'_`k''"
local next_leg "`leg`next'_`k''"
local `k'_at_1 "`date_`k''"
}
local input_words: list sizeof input

local leaves_here: word `from' of `airports_`k''
local arrives_here: word `to' of `airports_`k''

local airline: word `airln' of `input'
local flight: word `flightno' of `input'
local leaves_time: word `deptime' of `input'
local arrives_time: word 4 of `input'

local layover_from=0 // default arrival,
local layover_to =0 // departure and
local layover_next=0 // connecting flight on same day

// now see if any flight arrives/departs the next day
if `input_words'==6 {
local token: word 4 of `input'
if "`token'"=="next" {
local layover_from=1 // departure on next day
local arrives_time: word 6 of `input'
}
else {
local layover_to =1 // arrival on next day
}
}
if `input_words'==8 {
local arrives_time: word 6 of `input'
local layover_from=1 // departure on next day
local layover_to =1 // and also arrival on next day
}

local next: list sizeof next_leg
if `next'>4 {
local token: word 4 of `next_leg'
if "`token'"=="next" {
local layover_next=1
}
}

local layover_arrives=`layover_to'
local layover_leaves =`layover_from'

// build itinerary line `from'-`to' on leg `k'
foreach z in leaves arrives {
local token ""
local plane ""
if "`z'"=="leaves" {
local plane "`airline' `flight' "
}
if `layover_`z''==1 {
local token " next day"
}
local `z'_msg "`plane'`z' ``z'_here' at ``z'_time'`token'"
}

local msg "`leaves_here'_`arrives_here'_label"
local `msg' "`leaves_msg' `arrives_msg'"

// now calculate layover at node `to' on leg `k'
// cumulate all layovers up to this point for correct date
local layovers_`k' =`layovers_`k''+`layover_from'+`layover_to'
local date_getthere=td("`date_`k''")+`layovers_`k''
local date_leavethere=`date_getthere'+`layover_next'
foreach w in getthere leavethere {
local day =day(`date_`w'')
local month =month(`date_`w'')
local year =year(`date_`w'')
local `k'_at_`w' "`day'-`month'-`year'"
}
local subtr_this "``k'_at_getthere' `arrives_time'"
local subtr_from: word `deptime' of `next_leg'
local subtr_from "``k'_at_leavethere' `subtr_from'"
foreach z in this from {
local clock_`z'=clock("`subtr_`z''","DMYhm")
}
local minutes=minutes(`clock_from'-`clock_this')
local `to'_`k'_hours=int(hours(`clock_from'-`clock_this'))
local `to'_`k'_minutes=`minutes'-``to'_`k'_hours'*60
local `k'_at_`to' "``k'_at_getthere'"
}
}
}

// screen output
// #############

// display legs in reverse order
// on the way back

local go_there "leaving home"
local go_back "returning"

di "length of layovers displayed as [h]h:[m]m"
di ""
foreach k in `ways' {
if `legs_`k''>0 {
di ""
di "`go_`k'' on `date_`k''"
di ""
forvalues i=1/`legs_`k'' {
local from=`i'
local to=`i'+1

if "`k'"=="back" {
local j=`ends_`k''-`i'+1
local from=`j'
local to=`j'-1
}

local leaves_here: word `from' of `airports_`k''
local arrives_here: word `to' of `airports_`k''
di "``leaves_here'_`arrives_here'_label'"
if `to'!=1 & `to'!=`ends_`k'' {
local hrs=``to'_`k'_hours'
local min=``to'_`k'_minutes'
di "layover in `arrives_here' -- `hrs':`min' hour(s)"
}
}
di ""
di "arrive `k' on " %td date("``k'_at_`to''", "DMY")
}
}
di ""
di "Total cost is $" `cost'

end

This program handles multiple legs, departure and arrival on different dates, or one-way trips. It uses Stata's clock functions to calculate layovers accurately, across different days if need be; I prefer mine in whole hours and minutes. Stata's date and time functions recognize either the am/pm format or the 24-hour format. The thing works, as far as I can tell. I tested it with a few bogus itineraries and I had no surprises. The code appears properly indented in my original do-file. You'll have to do that yourself.

One of these days I will send the output straight to e-mail, so I can have my itinerary on my BlackBerry on one clean screenful, maybe two. It would be nice if input were via some web form, with Stata launched in batch mode once the user input is collected. But for now this thing will do for trip planning when I must buy tickets from different places for different legs, not that that happens very often.

Here's the output of the printItinerary Stata command as defined above:


length of layovers displayed as [h]h:[m]m

leaving home on 10dec2008

Delta 6742 leaves RDU at 4:00pm arrives JFK at 6:00pm
layover in JFK -- 2:0 hour(s)
Icelandair 614 leaves JFK at 8:00pm arrives KEF at 6:45am next day

arrive there on 11dec2008

returning on 14dec2008

Icelandair 615 leaves KEF at 5:05pm arrives JFK at 6:10pm
layover in JFK -- 1:0 hour(s)
Delta 6227 leaves JFK at 7:10pm arrives RDU at 9:30pm

arrive back on 14dec2008

Total cost is $2094