PageRenderTime 51ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/DDG/Spice/Flights/Route.pm

http://github.com/duckduckgo/zeroclickinfo-spice
Perl | 227 lines | 115 code | 53 blank | 59 comment | 22 complexity | 2e8160f27b50997c8865eeacc1da1abc MD5 | raw file
Possible License(s): Apache-2.0
  1. package DDG::Spice::Flights::Route;
  2. # ABSTRACT: Shows information on flights
  3. use strict;
  4. use DDG::Spice;
  5. use Text::Trim;
  6. # cache responses for 5 minutes
  7. spice proxy_cache_valid => "200 304 5m";
  8. spice from => '(.*)/(.*)/(.*)/(.*)/(.*)/(.*)/(.*)/(.*)/(.*)/(.*)/(.*)';
  9. spice to => 'https://api.flightstats.com/flex/flightstatus/rest/v2/jsonp/route/status/$4/$5/arr/$6/$7/$8?hourOfDay=$9&utc=true&appId={{ENV{DDG_SPICE_FLIGHTS_API_ID}}}&appKey={{ENV{DDG_SPICE_FLIGHTS_APIKEY}}}&callback={{callback}}';
  10. spice alt_to => {
  11. route_helper => {
  12. proxy_cache_valid => '200 304 5m',
  13. to => 'https://api.flightstats.com/flex/flightstatus/rest/v2/jsonp/route/status/$1/$2/$3/$4/$5/$6/?hourOfDay=$7&utc=true&appId={{ENV{DDG_SPICE_FLIGHTS_API_ID}}}&appKey={{ENV{DDG_SPICE_FLIGHTS_APIKEY}}}&callback={{callback}}',
  14. from => '(.*)/(.*)/(.*)/(.*)/(.*)/(.*)/(.*)'
  15. }
  16. };
  17. my @triggers = ('airport', 'international', 'national', 'intl', 'regional');
  18. # get the list of cities and their IATA airport codes
  19. my (%citiesByName, %citiesByCode);
  20. foreach my $line (share('cities.csv')->slurp) {
  21. # remove \n and separate line into fields
  22. chomp($line);
  23. my @line = split(/,/, $line);
  24. my $cityName = lc($line[3]);
  25. my $airportCode = lc($line[0]);
  26. # map city names to their airport codes; if this city name
  27. # already exists, append the new airport code to the end
  28. if (exists $citiesByName{$cityName}) {
  29. push(@{$citiesByName{$cityName}}, $airportCode);
  30. } else {
  31. $citiesByName{$cityName} = [$airportCode];
  32. }
  33. # map airport codes to city names
  34. $citiesByCode{$airportCode} = $cityName;
  35. # store both the city name and airport code as triggers
  36. push(@triggers, ($cityName, $airportCode));
  37. # remove common words from the airport name and add as a new key
  38. # that maps to the same airport code if the stripped airport name and the city name
  39. # are not identical
  40. my $strippedAirportName = lc($line[4]);
  41. if ($strippedAirportName ne $cityName) {
  42. if (exists $citiesByName{$strippedAirportName}) {
  43. push(@{$citiesByName{$strippedAirportName}}, $airportCode);
  44. } else {
  45. $citiesByName{$strippedAirportName} = [$airportCode];
  46. }
  47. push(@triggers, $strippedAirportName);
  48. }
  49. }
  50. # get the list of airlines and their ICAO codes
  51. my %airlines = ();
  52. foreach my $line (share('airlines.csv')->slurp) {
  53. # remove \n
  54. chomp($line);
  55. # associate airport name with its code
  56. my @line = split(/,/, $line);
  57. $airlines{lc($line[1])} = lc($line[0]);
  58. }
  59. triggers startend => @triggers;
  60. # identifyCodes(...) looks at either side of a flight query
  61. # and returns the airline and airport code, if found
  62. #
  63. # inputs:
  64. # [0] $query is the combined airline/city query that needs to be split
  65. # [1] $leftQuery is a boolean that indicates if $query is the source city
  66. # [2] $otherCity is the remainder of the original query on the other side of the
  67. # preposition
  68. #
  69. # outputs:
  70. # [0] array, ICAO airline codes
  71. # [1] array, originating IATA airport codes
  72. # [2] array, destination IATA airport codes
  73. # [3] source city
  74. # [4] destination city
  75. #
  76. # ICAO codes are supposed to be unique, while IATA codes do not have to be.
  77. # As the ICAO codes are only used internally, mapping airlines to their ICAO codes
  78. # makes filtering results easier. However, travelers usually always see an
  79. # airport's IATA code (e.g., Los Angeles's IATA code is LAX, and its ICAO code is KLAX),
  80. # so we will match airports by IATA.
  81. sub identifyCodes {
  82. my ($query, $leftQuery, $otherCity) = @_;
  83. # split query into individual words
  84. # at least two words are required (1 for the airline and 1 for the city)
  85. my @query = split(/\s+/, $query);
  86. return if scalar(@query) < 2;
  87. # an ambiguous airline name may return multiple airline codes
  88. # collect all airline names that begin with the user's query
  89. my @airlineCodes = ();
  90. foreach (0..$#query-1) {
  91. # search query word by word; for example, if the query that we need to
  92. # parse is "boston to los angeles jetblue", then we check
  93. # 1) "los" "angeles jetblue"
  94. # 2) "los angeles" "jetblue"
  95. my $groupA = join(' ', @query[0..$_]);
  96. my $groupB = join(' ', @query[$_+1..($#query)]);
  97. for my $airlineName (keys %airlines) {
  98. push(@airlineCodes, uc($airlines{$airlineName}))
  99. if $leftQuery
  100. and index($airlineName, $groupA) == 0
  101. and (exists $citiesByName{$groupB} or exists $citiesByCode{$groupB});
  102. push(@airlineCodes, uc($airlines{$airlineName}))
  103. if !$leftQuery
  104. and index($airlineName, $groupB) == 0
  105. and (exists $citiesByName{$groupA} or exists $citiesByCode{$groupA});
  106. }
  107. # [airline][city][to][city]
  108. return (join(",", @airlineCodes), $citiesByName{$groupB}, $citiesByName{$otherCity}, $groupB, $otherCity)
  109. if @airlineCodes and $leftQuery and exists $citiesByName{$groupB} and exists $citiesByName{$otherCity};
  110. # [city][to][city][airline]
  111. return (join(",", @airlineCodes), $citiesByName{$otherCity}, $citiesByName{$groupA}, $otherCity, $groupA)
  112. if @airlineCodes and !$leftQuery and exists $citiesByName{$otherCity} and exists $citiesByName{$groupA};
  113. # [airline][airport code][to][airport code]
  114. return (join(",", @airlineCodes), [$groupB], [$otherCity], $groupB, $otherCity)
  115. if @airlineCodes and $leftQuery and exists $citiesByCode{$groupB} and exists $citiesByCode{$otherCity};
  116. # [airport code][to][airport code][airline]
  117. return (join(",", @airlineCodes), [$otherCity], [$groupA], $otherCity, $groupA)
  118. if @airlineCodes and !$leftQuery and exists $citiesByCode{$otherCity} and exists $citiesByCode{$groupA};
  119. # [airline][airport code][to][city]
  120. return (join(",", @airlineCodes), [$groupB], $citiesByName{$otherCity}, $groupB, $otherCity)
  121. if @airlineCodes and $leftQuery and exists $citiesByCode{$groupB} and exists $citiesByName{$otherCity};
  122. # [airline][city][to][airport code]
  123. return (join(",", @airlineCodes), $citiesByName{$groupB}, [$otherCity], $groupB, $otherCity)
  124. if @airlineCodes and $leftQuery and exists $citiesByName{$groupB} and exists $citiesByCode{$otherCity};
  125. # [airport code][to][city][airline]
  126. return (join(",", @airlineCodes), [$otherCity], $citiesByName{$groupA}, $otherCity, $groupA)
  127. if @airlineCodes and !$leftQuery and exists $citiesByCode{$otherCity} and exists $citiesByName{$groupA};
  128. # [city][to][airport code][airline]
  129. return (join(",", @airlineCodes), $citiesByName{$otherCity}, [$groupA], $otherCity, $groupA)
  130. if @airlineCodes and !$leftQuery and exists $citiesByName{$otherCity} and exists $citiesByCode{$groupA};
  131. }
  132. return;
  133. }
  134. handle query_lc => sub {
  135. # clean up input; strip periods and common words,
  136. # replace all other non-letters with a space, strip trailing spaces
  137. s/\b(airport|national|international|intl|regional)\b//g;
  138. s/\.//g;
  139. s/\b[^a-z]+\b/ /g;
  140. trim;
  141. # query must be in the form [airline][city][to][city] or [city][to][city][airline]
  142. my @query = split(/\s+to\s+/, $_);
  143. return if scalar(@query) != 2;
  144. # strip 'fligh(s) from' to allow more flexible queries
  145. $query[0] =~ s/\b(flights?|from)\b//g;
  146. trim($_) foreach @query;
  147. # get the current time, minus six hours
  148. my ($second, $minute, $hour, $dayOfMonth,
  149. $month, $year, $dayOfWeek, $dayOfYear, $daylightSavings) = gmtime(time - 21600);
  150. $month += 1;
  151. $year += 1900;
  152. my @flightCodes = ();
  153. # query format: [airline][city or airport code][to][city or airport code]
  154. if (exists $citiesByName{$query[1]} or exists $citiesByCode{$query[1]}) {
  155. @flightCodes = identifyCodes($query[0], 1, $query[1]);
  156. # query format: [city or airport code][to][city or airport code][airline]
  157. } elsif (exists $citiesByName{$query[0]} or exists $citiesByCode{$query[0]}) {
  158. @flightCodes = identifyCodes($query[1], 0, $query[0]);
  159. }
  160. return unless @flightCodes;
  161. # prepare strings for "more information at" links
  162. my $sourceCity = $flightCodes[3];
  163. my $destinationCity = $flightCodes[4];
  164. $sourceCity =~ s/\s+/+/g;
  165. $destinationCity =~ s/\s+/+/g;
  166. return ($flightCodes[0],
  167. join(",", map(uc, @{$flightCodes[1]})),
  168. join(",", map(uc, @{$flightCodes[2]})),
  169. uc($flightCodes[1][0]), uc($flightCodes[2][0]),
  170. $year, $month, $dayOfMonth, $hour,
  171. $sourceCity, $destinationCity
  172. );
  173. };
  174. 1;