From 083d98e4315574a4d556d3fc8184c71ae1234ef9 Mon Sep 17 00:00:00 2001 From: mesozoic-drones <mesozoic.drones@gmail.com> Date: Sat, 28 Mar 2020 12:05:20 +0300 Subject: [PATCH] Update README.md --- .clang-format | 31 + .github/workflows/ccpp.yml | 31 + .gitignore | 9 + .gitmodules | 3 + CMakeLists.txt | 15 + LICENSE => LICENSE.MIT | 0 README.md | 112 +- benchmarks/CMakeLists.txt | 0 docs/CPP_STYLE.md | 6 + docs/logo.jpeg | Bin 0 -> 46868 bytes doctest | 1 + include/just_gtfs/just_gtfs.h | 1923 ++++++++++++++++++++ tests/CMakeLists.txt | 10 + tests/data/sample_feed/agency.txt | 2 + tests/data/sample_feed/calendar.txt | 3 + tests/data/sample_feed/calendar_dates.txt | 2 + tests/data/sample_feed/fare_attributes.txt | 3 + tests/data/sample_feed/fare_rules.txt | 5 + tests/data/sample_feed/frequencies.txt | 12 + tests/data/sample_feed/routes.txt | 6 + tests/data/sample_feed/shapes.txt | 9 + tests/data/sample_feed/stop_times.txt | 29 + tests/data/sample_feed/stops.txt | 10 + tests/data/sample_feed/trips.txt | 12 + tests/unit_tests.cpp | 345 ++++ 25 files changed, 2577 insertions(+), 2 deletions(-) create mode 100644 .clang-format create mode 100644 .github/workflows/ccpp.yml create mode 100644 .gitmodules create mode 100644 CMakeLists.txt rename LICENSE => LICENSE.MIT (100%) create mode 100644 benchmarks/CMakeLists.txt create mode 100644 docs/CPP_STYLE.md create mode 100644 docs/logo.jpeg create mode 160000 doctest create mode 100644 include/just_gtfs/just_gtfs.h create mode 100644 tests/CMakeLists.txt create mode 100644 tests/data/sample_feed/agency.txt create mode 100644 tests/data/sample_feed/calendar.txt create mode 100644 tests/data/sample_feed/calendar_dates.txt create mode 100644 tests/data/sample_feed/fare_attributes.txt create mode 100644 tests/data/sample_feed/fare_rules.txt create mode 100644 tests/data/sample_feed/frequencies.txt create mode 100644 tests/data/sample_feed/routes.txt create mode 100644 tests/data/sample_feed/shapes.txt create mode 100644 tests/data/sample_feed/stop_times.txt create mode 100644 tests/data/sample_feed/stops.txt create mode 100644 tests/data/sample_feed/trips.txt create mode 100644 tests/unit_tests.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..a82ec7f --- /dev/null +++ b/.clang-format @@ -0,0 +1,31 @@ +# Configuration file for clang-format, based on docs/CPP_STYLE.md. + +BasedOnStyle: Google +IndentWidth: 2 +BreakBeforeBraces: Allman +ColumnLimit: 100 + +Language: Cpp +AccessModifierOffset: -2 +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: true +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +BreakConstructorInitializersBeforeComma: true +ConstructorInitializerIndentWidth: 4 +DerivePointerAlignment: false +IndentCaseLabels: false +NamespaceIndentation: None +PointerAlignment: Middle +SortIncludes: true +Standard: Cpp11 + +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 diff --git a/.github/workflows/ccpp.yml b/.github/workflows/ccpp.yml new file mode 100644 index 0000000..75af05f --- /dev/null +++ b/.github/workflows/ccpp.yml @@ -0,0 +1,31 @@ +name: C/C++ CI + +on: + push: + branches: [ master, add-* ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: git_actions + run: git submodule update --init --recursive + - name: cmake + run: | + sudo apt update + sudo apt install mm-common g++-9 + export CXX=g++-9 + cmake . + - name: make + run: | + export CXX=g++-9 + make + - name: run_tests + run: | + pwd + ctest --output-on-failure diff --git a/.gitignore b/.gitignore index 259148f..36b4117 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,12 @@ *.exe *.out *.app + +# Other +.DS_Store +.idea/ +cmake-build-debug/ +CMakeFiles/ +Makefile +*.cmake +CMakeCache.txt diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3906885 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "doctest"] + path = doctest + url = https://github.com/onqtam/doctest diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b9558e6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.6) + +project(just_gtfs LANGUAGES CXX VERSION 0.1) + +include_directories(include) +include_directories(doctest/doctest) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED on) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror") + +enable_testing() + +add_subdirectory(tests) +add_subdirectory(benchmarks) diff --git a/LICENSE b/LICENSE.MIT similarity index 100% rename from LICENSE rename to LICENSE.MIT diff --git a/README.md b/README.md index cf960f2..be2e8af 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,110 @@ -# just_gtfs -C++17 header-only GTFS parsing library +# just_gtfs - header-only modern C++ GTFS parsing library + +[](https://github.com/mapsme/just_gtfs) + +[](https://shields.io/) +[](https://lbesson.mit-license.org/) + +[](https://github.com/mapsme/just_gtfs/issues) + + - Header-only + - C++17 + - Tested on GCC and Clang + - STL-compatible containers + - Fast reading and parsing of GTFS feeds + +## Table of Contents +- [Working with GTFS feeds](#working-with-gtfs-feeds) +- [How to use just_library](#how-to-use-it) +- [Used third-party tools](#used-third-party-tools) + +## Working with GTFS feeds +The library implements reading static transit data in GTFS - [General Transit Feed Specification](https://developers.google.com/transit/gtfs/reference). +It provides class for working with GTFS feeds: `gtfs::Feed`. +GTFS csv files are mapped to the corresponding C++ classes. Every GTFS entity can be accessed through `gtfs::Feed`. + +:pushpin: Example of providing `gtfs::Feed` the feed path, reading it and working with GTFS entities such as stops and routes: +```c++ +Feed feed("~/data/SFMTA/"); +if (feed.read_feed() == ResultCode::OK) +{ + Stops stops = feed.get_stops(); + std::cout << stops.size() << std::endl; + + Route route = feed.get_route("route_id_1009"); + if (route) + { + std::cout << route->route_long_name << std::endl; + } +} +``` + +GTFS feed can be wholly read from directory as in the example above or you can read GTFS files separately. E.g., if you need only shapes data, you can avoid parsing all other files and just work with the shapes. + +:pushpin: Example of reading only `shapes.txt` from the feed and working with shapes: +```c++ +Feed feed("~/data/SFMTA/"); +if (feed.read_shapes() == ResultCode::OK) +{ + Shapes all_shapes = feed.get_shapes(); + Shape shape = feed.get_shape("9367"); +} +``` + + +## Methods for reading and writing GTFS entities +Methods of the `Feed` class for working with agencies: + +Read agencies from the corresponding csv file. +```c++ +Result read_agencies() +``` + +Get reference to `Agencies` - `std::vector` of `Agency` objects. +```c++ +const Agencies & get_agencies() +``` + +Find agency by its id. This method returns `std::optional` so you should check if the result is `std::nullopt`. +```c++ +std::optional<Agency> get_agency(const Id & agency_id) +``` + +Add agency to the feed. +```c++ +void add_agency(const Agency & agency) +``` + +Add agency to the feed by filling agency object fields with parsed csv values. `row` is `std::map` with csv column titles as keys ans csv row items as values. +```c++ +Result add_agency(ParsedCsvRow const & row) +``` + + +:pushpin: **There are similar methods for all other GTFS entities** for getting the list of entities, finding and adding them. +For some of them additional methods are provided. +For example, you can find all the stop times for current stop by its id: +```c++ +StopTimes get_stop_times_for_stop(const Id & stop_id) +``` + +Or you can find stop times for the particular trip: +```c++ +StopTimes get_stop_times_for_trip(const Id & trip_id, bool sort_by_sequence = true) +``` + +## How to use library +- For including the library in your own project: just_gtfs is completely contained inside a single header and therefore it is sufficient to copy include/just_gtfs/just_gtfs.h to your include pathes. The library does not have to be explicitly build. +- For running library tests: +Clone just_gtfs with `git clone --recursive` or run `git submodule update --init --recursive` after cloning. +In the just_gtfs project directory build the project and run unit tests: +``` +cmake . +make +ctest --output-on-failure +``` +The library makes use of the C++17 features and therefore you have to use appropriate compiler version. +- For including as a submodule: use branch "for-usage-as-submodule" which consists of a single header. + +## Used third-party tools +- [**doctest**](https://github.com/onqtam/doctest) for unit testing. diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/docs/CPP_STYLE.md b/docs/CPP_STYLE.md new file mode 100644 index 0000000..1b51fd3 --- /dev/null +++ b/docs/CPP_STYLE.md @@ -0,0 +1,6 @@ +## C++ Style Guide + +We use C++ code style similar to the [MAPS.ME project](https://github.com/mapsme/omim/blob/master/docs/CPP_STYLE.md) with some differences: +- Use **CamelCase** for class names and **snake_case** for other entities like methods, variables, etc. +- Use left-to-right order for variables/params: `const std::string & s` (reference to the const string). +- Do not use prefixes like `m_` for member variables. diff --git a/docs/logo.jpeg b/docs/logo.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..22ad621e3b8c2a493e0dc7e970855fedd6d41716 GIT binary patch literal 46868 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 literal 0 HcmV?d00001 diff --git a/doctest b/doctest new file mode 160000 index 0000000..932a2ca --- /dev/null +++ b/doctest @@ -0,0 +1 @@ +Subproject commit 932a2ca50666138256dae56fbb16db3b1cae133a diff --git a/include/just_gtfs/just_gtfs.h b/include/just_gtfs/just_gtfs.h new file mode 100644 index 0000000..3817c5e --- /dev/null +++ b/include/just_gtfs/just_gtfs.h @@ -0,0 +1,1923 @@ +#pragma once + +#include <algorithm> +#include <cassert> +#include <cstdint> +#include <exception> +#include <filesystem> +#include <fstream> +#include <functional> +#include <istream> +#include <map> +#include <optional> +#include <stdexcept> +#include <string> +#include <tuple> +#include <unordered_map> +#include <unordered_set> +#include <utility> +#include <vector> + +namespace gtfs +{ +// Helper classes ---------------------------------------------------------------------------------- +struct InvalidFieldFormat : public std::exception +{ +public: + explicit InvalidFieldFormat(const std::string & msg) : message(prefix + msg) {} + + const char * what() const noexcept + { + return message.c_str(); + } + +private: + const std::string prefix = "Invalid GTFS field format. "; + std::string message; +}; + +enum ResultCode +{ + OK, + END_OF_FILE, + ERROR_INVALID_GTFS_PATH, + ERROR_FILE_ABSENT, + ERROR_REQUIRED_FIELD_ABSENT, + ERROR_INVALID_FIELD_FORMAT +}; + +using Message = std::string; + +struct Result +{ + ResultCode code = OK; + Message message; + + bool operator==(ResultCode result_code) const { return code == result_code; } + bool operator!=(ResultCode result_code) const { return !(code == result_code); } +}; + +// Csv parser ------------------------------------------------------------------------------------- +class CsvParser +{ +public: + CsvParser() = default; + inline explicit CsvParser(const std::string & gtfs_directory); + + inline Result read_header(const std::string & csv_filename); + inline Result read_row(std::map<std::string, std::string> & obj); + + inline static std::vector<std::string> split_record(const std::string & record, + bool is_header = false); + +private: + std::vector<std::string> field_sequence; + std::filesystem::path gtfs_path; + std::ifstream csv_stream; + static const char delimiter = ','; +}; + +inline CsvParser::CsvParser(const std::string & gtfs_directory) : gtfs_path(gtfs_directory) {} + +inline void trim_spaces(std::string & token) +{ + while (!token.empty() && token.back() == ' ') + token.pop_back(); +} + +inline std::vector<std::string> CsvParser::split_record(const std::string & record, bool is_header) +{ + std::string const delims = "\r\t"; + size_t start_index = 0; + if (is_header) + { + // ignore UTF-8 BOM prefix: + if (record.size() > 2 && record[0] == '\xef' && record[1] == '\xbb' && record[2] == '\xbf') + start_index = 3; + } + std::vector<std::string> fields; + fields.reserve(20); + + std::string token; + token.reserve(record.size()); + + size_t token_start_index = start_index; + bool is_inside_quotes = false; + + for (size_t i = start_index; i < record.size(); ++i) + { + if (record[i] == '"') + { + is_inside_quotes = !is_inside_quotes; + continue; + } + + if (record[i] == ' ') + { + if (token_start_index == i) + token_start_index = i + 1; + else + token += record[i]; + continue; + } + + if (record[i] == delimiter) + { + if (is_inside_quotes) + { + token += record[i]; + continue; + } + token_start_index = i + 1; + trim_spaces(token); + fields.push_back(token); + token.erase(); + continue; + } + + if (delims.find(record[i]) == std::string::npos) + token += record[i]; + } + trim_spaces(token); + fields.push_back(token); + return fields; +} + +inline Result CsvParser::read_header(const std::string & csv_filename) +{ + if (csv_stream.is_open()) + csv_stream.close(); + + csv_stream.open(gtfs_path / csv_filename); + if (!csv_stream.is_open()) + return {ResultCode::ERROR_FILE_ABSENT, {}}; + + std::string header; + if (!getline(csv_stream, header) || header.empty()) + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, {}}; + + field_sequence = split_record(header, true); + return {ResultCode::OK, {}}; +} + +inline Result CsvParser::read_row(std::map<std::string, std::string> & obj) +{ + std::string row; + if (!getline(csv_stream, row)) + { + obj = {}; + return {ResultCode::END_OF_FILE, {}}; + } + + if (row == "\r") + { + obj = {}; + return {ResultCode::OK, {}}; + } + + std::vector<std::string> fields_values = split_record(row); + + // Different count of fields in row and in the header of csv. + // Typical approach to skip not required fields. + if (fields_values.size() != field_sequence.size()) + obj = {}; + + for (size_t i = 0; i < field_sequence.size(); ++i) + obj[field_sequence[i]] = fields_values[i]; + + return {ResultCode::OK, {}}; +} + +// Custom types for GTFS fields -------------------------------------------------------------------- +// Id of GTFS entity, a sequence of any UTF-8 characters. Used as type for ID GTFS fields. +using Id = std::string; +// A string of UTF-8 characters. Used as type for Text GTFS fields. +using Text = std::string; + +// Time in GTFS is in the HH:MM:SS format (H:MM:SS is also accepted) +// Time within a service day can be above 24:00:00, e.g. 28:41:30 +class Time +{ +public: + inline Time() = default; + inline explicit Time(const std::string & raw_time_str); + inline Time(uint16_t hours, uint16_t minutes, uint16_t seconds); + inline bool is_provided() const; + inline size_t get_total_seconds() const; + inline std::tuple<uint16_t, uint16_t, uint16_t> get_hh_mm_ss() const; + inline std::string get_raw_time() const; + inline bool limit_hours_to_24max(); + +private: + inline void set_total_seconds(); + inline void set_raw_time(); + bool time_is_provided = false; + std::string raw_time; + size_t total_seconds = 0; + uint16_t hh = 0; + uint16_t mm = 0; + uint16_t ss = 0; +}; + +inline bool operator==(const Time & lhs, const Time & rhs) +{ + return lhs.get_hh_mm_ss() == rhs.get_hh_mm_ss() && lhs.is_provided() == rhs.is_provided(); +} + +inline bool Time::limit_hours_to_24max() +{ + if (hh < 24) + return false; + + hh = hh % 24; + set_total_seconds(); + set_raw_time(); + return true; +} + +inline void Time::set_total_seconds() +{ + total_seconds = hh * 60 * 60 + mm * 60 + ss; +} + +inline std::string append_leading_zero(const std::string & s, bool check = true) +{ + if (check && s.size() > 2) + throw InvalidFieldFormat("The string for appending zero is too long: " + s); + + if (s.size() == 2) + return s; + return "0" + s; +} + +inline void Time::set_raw_time() +{ + const std::string hh_str = append_leading_zero(std::to_string(hh), false); + const std::string mm_str = append_leading_zero(std::to_string(mm)); + const std::string ss_str = append_leading_zero(std::to_string(ss)); + + raw_time = hh_str + ":" + mm_str + ":" + ss_str; +} + +// Time in the HH:MM:SS format (H:MM:SS is also accepted). Used as type for Time GTFS fields. +inline Time::Time(const std::string & raw_time_str) : raw_time(raw_time_str) +{ + if (raw_time_str.empty()) + return; + + const size_t len = raw_time.size(); + if (!(len == 7 || len == 8) || (raw_time[len - 3] != ':' && raw_time[len - 6] != ':')) + throw InvalidFieldFormat("Time is not in [H]H:MM:SS format: " + raw_time_str); + + hh = static_cast<uint16_t>(std::stoi(raw_time.substr(0, len - 6))); + mm = static_cast<uint16_t>(std::stoi(raw_time.substr(len - 5, 2))); + ss = static_cast<uint16_t>(std::stoi(raw_time.substr(len - 2))); + + if (mm > 60 || ss > 60) + throw InvalidFieldFormat("Time minutes/seconds wrong value: " + std::to_string(mm) + + " minutes, " + std::to_string(ss) + " seconds"); + + set_total_seconds(); + time_is_provided = true; +} + +inline Time::Time(uint16_t hours, uint16_t minutes, uint16_t seconds) + : hh(hours), mm(minutes), ss(seconds) +{ + if (mm > 60 || ss > 60) + throw InvalidFieldFormat("Time is out of range: " + std::to_string(mm) + "minutes " + + std::to_string(ss) + "seconds"); + + set_total_seconds(); + set_raw_time(); + time_is_provided = true; +} + +inline bool Time::is_provided() const { return time_is_provided; } + +inline size_t Time::get_total_seconds() const { return total_seconds; } + +inline std::tuple<uint16_t, uint16_t, uint16_t> Time::get_hh_mm_ss() const { return {hh, mm, ss}; } + +inline std::string Time::get_raw_time() const { return raw_time; } + +// Service day in the YYYYMMDD format. +class Date +{ +public: + inline Date() = default; + inline Date(uint16_t year, uint16_t month, uint16_t day); + inline explicit Date(const std::string & raw_date_str); + inline bool is_provided() const; + inline std::tuple<uint16_t, uint16_t, uint16_t> get_yyyy_mm_dd() const; + inline std::string get_raw_date() const; + +private: + inline void check_valid() const; + + std::string raw_date; + uint16_t yyyy = 0; + uint16_t mm = 0; + uint16_t dd = 0; + bool date_is_provided = false; +}; + +inline bool operator==(const Date & lhs, const Date & rhs) +{ + return lhs.get_yyyy_mm_dd() == rhs.get_yyyy_mm_dd() && lhs.is_provided() == rhs.is_provided(); +} + +inline void Date::check_valid() const +{ + if (yyyy < 1000 || yyyy > 9999 || mm < 1 || mm > 12 || dd < 1 || dd > 31) + throw InvalidFieldFormat("Date check failed: out of range. " + std::to_string(yyyy) + + " year, " + std::to_string(mm) + " month, " + std::to_string(dd) + + " day"); + + if (mm == 2 && dd > 28) + { + // The year is not leap. Days count should be 28. + if (yyyy % 4 != 0 || (yyyy % 100 == 0 && yyyy % 400 != 0)) + throw InvalidFieldFormat("Invalid days count in February of non-leap year: " + + std::to_string(dd) + " year" + std::to_string(yyyy)); + + // The year is leap. Days count should be 29. + if (dd > 29) + throw InvalidFieldFormat("Invalid days count in February of leap year: " + + std::to_string(dd) + " year" + std::to_string(yyyy)); + } + + if (dd > 30 && (mm == 4 || mm == 6 || mm == 9 || mm == 11)) + throw InvalidFieldFormat("Invalid days count in month: " + std::to_string(dd) + " days in " + + std::to_string(mm)); +} + +inline Date::Date(uint16_t year, uint16_t month, uint16_t day) : yyyy(year), mm(month), dd(day) +{ + check_valid(); + const std::string mm_str = append_leading_zero(std::to_string(mm)); + const std::string dd_str = append_leading_zero(std::to_string(dd)); + + raw_date = std::to_string(yyyy) + mm_str + dd_str; + date_is_provided = true; +} + +inline Date::Date(const std::string & raw_date_str) : raw_date(raw_date_str) +{ + if (raw_date.empty()) + return; + + if (raw_date.size() != 8) + throw InvalidFieldFormat("Date is not in YYYY:MM::DD format: " + raw_date_str); + + yyyy = static_cast<uint16_t>(std::stoi(raw_date.substr(0, 4))); + mm = static_cast<uint16_t>(std::stoi(raw_date.substr(4, 2))); + dd = static_cast<uint16_t>(std::stoi(raw_date.substr(6, 2))); + + check_valid(); + + date_is_provided = true; +} + +inline bool Date::is_provided() const { return date_is_provided; } + +inline std::tuple<uint16_t, uint16_t, uint16_t> Date::get_yyyy_mm_dd() const +{ + return {yyyy, mm, dd}; +} + +inline std::string Date::get_raw_date() const { return raw_date; } + +// An ISO 4217 alphabetical currency code. Used as type for Currency Code GTFS fields. +using CurrencyCode = std::string; +// An IETF BCP 47 language code. Used as type for Language Code GTFS fields. +using LanguageCode = std::string; + +// Helper enums for some GTFS fields --------------------------------------------------------------- +enum class StopLocationType +{ + StopOrPlatform = 0, + Station = 1, + EntranceExit = 2, + GenericNode = 3, + BoardingArea = 4 +}; + +// The type of transportation used on a route. +enum class RouteType +{ + // GTFS route types + Tram = 0, // Tram, Streetcar, Light rail + Subway = 1, // Any underground rail system within a metropolitan area + Rail = 2, // Intercity or long-distance travel + Bus = 3, // Short- and long-distance bus routes + Ferry = 4, // Boat service + CableTram = 5, // Street-level rail cars where the cable runs beneath the vehicle + AerialLift = 6, // Aerial lift, suspended cable car (gondola lift, aerial tramway) + Funicular = 7, // Any rail system designed for steep inclines + Trolleybus = 11, // Electric buses that draw power from overhead wires using poles + Monorail = 12, // Railway in which the track consists of a single rail or a beam + + // Extended route types + // https://developers.google.com/transit/gtfs/reference/extended-route-types + RailwayService = 100, + HighSpeedRailService = 101, + LongDistanceTrains = 102, + InterRegionalRailService = 103, + CarTransportRailService = 104, + SleeperRailService = 105, + RegionalRailService = 106, + TouristRailwayService = 107, + RailShuttleWithinComplex = 108, + SuburbanRailway = 109, + ReplacementRailService = 110, + SpecialRailService = 111, + LorryTransportRailService = 112, + AllRailServices = 113, + CrossCountryRailService = 114, + VehicleTransportRailService = 115, + RackAndPinionRailway = 116, + AdditionalRailService = 117, + + CoachService = 200, + InternationalCoachService = 201, + NationalCoachService = 202, + ShuttleCoachService = 203, + RegionalCoachService = 204, + SpecialCoachService = 205, + SightseeingCoachService = 206, + TouristCoachService = 207, + CommuterCoachService = 208, + AllCoachServices = 209, + + UrbanRailwayService400 = 400, + MetroService = 401, + UndergroundService = 402, + UrbanRailwayService403 = 403, + AllUrbanRailwayServices = 404, + Monorail405 = 405, + + BusService = 700, + RegionalBusService = 701, + ExpressBusService = 702, + StoppingBusService = 703, + LocalBusService = 704, + NightBusService = 705, + PostBusService = 706, + SpecialNeedsBus = 707, + MobilityBusService = 708, + MobilityBusForRegisteredDisabled = 709, + SightseeingBus = 710, + ShuttleBus = 711, + SchoolBus = 712, + SchoolAndPublicServiceBus = 713, + RailReplacementBusService = 714, + DemandAndResponseBusService = 715, + AllBusServices = 716, + + TrolleybusService = 800, + + TramService = 900, + CityTramService = 901, + LocalTramService = 902, + RegionalTramService = 903, + SightseeingTramService = 904, + ShuttleTramService = 905, + AllTramServices = 906, + + WaterTransportService = 1000, + AirService = 1100, + FerryService = 1200, + AerialLiftService = 1300, + FunicularService = 1400, + TaxiService = 1500, + CommunalTaxiService = 1501, + WaterTaxiService = 1502, + RailTaxiService = 1503, + BikeTaxiService = 1504, + LicensedTaxiService = 1505, + PrivateHireServiceVehicle = 1506, + AllTaxiServices = 1507, + MiscellaneousService = 1700, + HorseDrawnCarriage = 1702 +}; + +enum class TripDirectionId +{ + DefaultDirection = 0, // e.g. outbound + OppositeDirection = 1 // e.g. inbound +}; + +enum class TripAccess +{ + NoInfo = 0, + Yes = 1, + No = 2 +}; + +enum class StopTimeBoarding +{ + RegularlyScheduled = 0, + No = 1, // Not available + Phone = 2, // Must phone agency to arrange + CoordinateWithDriver = 3 // Must coordinate with driver to arrange +}; + +enum class StopTimePoint +{ + Approximate = 0, + Exact = 1 +}; + +enum class CalendarAvailability +{ + NotAvailable = 0, + Available = 1 +}; + +enum class CalendarDateException +{ + Added = 1, // Service has been added for the specified date + Removed = 2 +}; + +enum class FarePayment +{ + OnBoard = 0, + BeforeBoarding = 1 // Fare must be paid before boarding +}; + +enum class FareTransfers +{ + No = 0, // No transfers permitted on this fare + Once = 1, + Twice = 2, + Unlimited = 3 +}; + +enum class FrequencyTripService +{ + FrequencyBased = 0, // Frequency-based trips + ScheduleBased = 1 // Schedule-based trips with the exact same headway throughout the day +}; + +enum class TransferType +{ + Recommended = 0, + Timed = 1, + MinimumTime = 2, + NotPossible = 3 +}; + +enum class PathwayMode +{ + Walkway = 1, + Stairs = 2, + MovingSidewalk = 3, // Moving sidewalk/travelator + Escalator = 4, + Elevator = 5, + FareGate = 6, // Payment gate + ExitGate = 7 +}; + +enum class PathwayDirection +{ + Unidirectional = 0, + Bidirectional = 1 +}; + +enum class TranslationTable +{ + Agency = 0, + Stops, + Routes, + Trips, + StopTimes, + FeedInfo +}; + +enum class AttributionRole +{ + No = 0, // Organization doesn’t have this role + Yes = 1 // Organization does have this role +}; + +// Structures representing GTFS entities ----------------------------------------------------------- +// Required dataset file +struct Agency +{ + // Conditionally optional: + Id agency_id; + + // Required: + Text agency_name; + Text agency_url; + Text agency_timezone; + + // Optional: + Text agency_lang; + Text agency_phone; + Text agency_fare_url; + Text agency_email; +}; + +// Required dataset file +struct Stop +{ + // Required: + Id stop_id; + + // Conditionally required: + Text stop_name; + + bool coordinates_present = true; + double stop_lat = 0.0; + double stop_lon = 0.0; + Id zone_id; + Id parent_station; + + // Optional: + Text stop_code; + Text stop_desc; + Text stop_url; + StopLocationType location_type = StopLocationType::GenericNode; + Text stop_timezone; + Text wheelchair_boarding; + Id level_id; + Text platform_code; +}; + +// Required dataset file +struct Route +{ + // Required: + Id route_id; + RouteType route_type = RouteType::Tram; + + // Conditionally required: + Id agency_id; + Text route_short_name; + Text route_long_name; + + // Optional + Text route_desc; + Text route_url; + Text route_color; + Text route_text_color; + size_t route_sort_order = 0; // Routes with smaller value values should be displayed first +}; + +// Required dataset file +struct Trip +{ + // Required: + Id route_id; + Id service_id; + Id trip_id; + + // Optional: + Text trip_headsign; + Text trip_short_name; + TripDirectionId direction_id = TripDirectionId::DefaultDirection; + Id block_id; + Id shape_id; + TripAccess wheelchair_accessible = TripAccess::NoInfo; + TripAccess bikes_allowed = TripAccess::NoInfo; +}; + +// Required dataset file +struct StopTime +{ + // Required: + Id trip_id; + Id stop_id; + size_t stop_sequence = 0; + + // Conditionally required: + Time arrival_time; + + Time departure_time; + + // Optional: + Text stop_headsign; + StopTimeBoarding pickup_type = StopTimeBoarding::RegularlyScheduled; + StopTimeBoarding drop_off_type = StopTimeBoarding::RegularlyScheduled; + + double shape_dist_traveled = 0.0; + StopTimePoint timepoint = StopTimePoint::Exact; +}; + +// Conditionally required dataset file: +struct CalendarItem +{ + // Required: + Id service_id; + + CalendarAvailability monday = CalendarAvailability::NotAvailable; + CalendarAvailability tuesday = CalendarAvailability::NotAvailable; + CalendarAvailability wednesday = CalendarAvailability::NotAvailable; + CalendarAvailability thursday = CalendarAvailability::NotAvailable; + CalendarAvailability friday = CalendarAvailability::NotAvailable; + CalendarAvailability saturday = CalendarAvailability::NotAvailable; + CalendarAvailability sunday = CalendarAvailability::NotAvailable; + + Date start_date; + Date end_date; +}; + +// Conditionally required dataset file +struct CalendarDate +{ + // Required: + Id service_id; + Date date; + CalendarDateException exception_type = CalendarDateException::Added; +}; + +// Optional dataset file +struct FareAttribute +{ + // Required: + Id fare_id; + double price = 0.0; + CurrencyCode currency_code; + FarePayment payment_method = FarePayment::BeforeBoarding; + FareTransfers transfers = FareTransfers::Unlimited; + + // Conditionally required: + Id agency_id; + + // Optional: + size_t transfer_duration = 0; // Length of time in seconds before a transfer expires +}; + +// Optional dataset file +struct FareRule +{ + // Required: + Id fare_id; + + // Optional: + Id route_id; + Id origin_id; + Id destination_id; + Id contains_id; +}; + +// Optional dataset file +struct ShapePoint +{ + // Required: + Id shape_id; + double shape_pt_lat = 0.0; + double shape_pt_lon = 0.0; + size_t shape_pt_sequence = 0; + + // Optional: + double shape_dist_traveled = 0; +}; + +// Optional dataset file +struct Frequency +{ + // Required: + Id trip_id; + Time start_time; + Time end_time; + size_t headway_secs = 0; + + // Optional: + FrequencyTripService exact_times = FrequencyTripService::FrequencyBased; +}; + +// Optional dataset file +struct Transfer +{ + // Required: + Id from_stop_id; + Id to_stop_id; + TransferType transfer_type = TransferType::Recommended; + + // Optional: + size_t min_transfer_time = 0; +}; + +// Optional dataset file for the GTFS-Pathways extension +struct Pathway +{ + // Required: + Id pathway_d; + Id from_stop_id; + Id to_stop_id; + PathwayMode pathway_mode = PathwayMode::Walkway; + PathwayDirection is_bidirectional = PathwayDirection::Unidirectional; + + // Optional fields: + // Horizontal length in meters of the pathway from the origin location + double length = 0.0; + // Average time in seconds needed to walk through the pathway from the origin location + size_t traversal_time = 0; + // Number of stairs of the pathway + size_t stair_count = 0; + // Maximum slope ratio of the pathway + double max_slope = 0.0; + // Minimum width of the pathway in meters + double min_width = 0.0; + // Text from physical signage visible to transit riders + Text signposted_as; + // Same as signposted_as, but when the pathways is used backward + Text reversed_signposted_as; +}; + +// Optional dataset file +struct Level +{ + // Required: + Id level_id; + + // Numeric index of the level that indicates relative position of this level in relation to other + // levels (levels with higher indices are assumed to be located above levels with lower indices). + // Ground level should have index 0, with levels above ground indicated by positive indices and + // levels below ground by negative indices + double level_index = 0.0; + + // Optional: + Text level_name; +}; + +// Optional dataset file +struct FeedInfo +{ + // Required: + Text feed_publisher_name; + Text feed_publisher_url; + LanguageCode feed_lang; + + // Optional: + Date feed_start_date; + Date feed_end_date; + Text feed_version; + Text feed_contact_email; + Text feed_contact_url; +}; + +// Optional dataset file +struct Translation +{ + // Required: + TranslationTable table_name = TranslationTable::Agency; + Text field_name; + LanguageCode language; + Text translation; + + // Conditionally required: + Id record_id; + Id record_sub_id; + Text field_value; +}; + +// Optional dataset file +struct Attribution +{ + // Required: + Text organization_name; + + // Optional: + Id attribution_id; // Useful for translations + Id agency_id; + Id route_id; + Id trip_id; + + AttributionRole is_producer = AttributionRole::No; + AttributionRole is_operator = AttributionRole::No; + AttributionRole is_authority = AttributionRole::No; + + Text attribution_url; + Text attribution_email; + Text attribution_phone; +}; + +// Main classes for working with GTFS feeds +using Agencies = std::vector<Agency>; +using Stops = std::vector<Stop>; +using Routes = std::vector<Route>; +using Trips = std::vector<Trip>; +using StopTimes = std::vector<StopTime>; +using Calendar = std::vector<CalendarItem>; +using CalendarDates = std::vector<CalendarDate>; + +using FareRules = std::vector<FareRule>; +using Shapes = std::vector<ShapePoint>; +using Shape = std::vector<ShapePoint>; +using Frequencies = std::vector<Frequency>; +using Transfers = std::vector<Transfer>; +using Pathways = std::vector<Pathway>; +using Levels = std::vector<Level>; +// FeedInfo is a unique object and doesn't need a container. +using Translations = std::vector<Translation>; +using Attributions = std::vector<Attribution>; + +using ParsedCsvRow = std::map<std::string, std::string>; + +class Feed +{ +public: + inline Feed() = default; + inline explicit Feed(const std::string & gtfs_path); + + inline Result read_feed(); + + inline Result write_feed(const std::string & gtfs_path = {}) const; + + inline Result read_agencies(); + inline const Agencies & get_agencies() const; + inline std::optional<Agency> get_agency(const Id & agency_id) const; + inline void add_agency(const Agency & agency); + + inline Result read_stops(); + inline const Stops & get_stops() const; + inline std::optional<Stop> get_stop(const Id & stop_id) const; + inline void add_stop(const Stop & stop); + + inline Result read_routes(); + inline const Routes & get_routes() const; + inline std::optional<Route> get_route(const Id & route_id) const; + inline void add_route(const Route & route); + + inline Result read_trips(); + inline const Trips & get_trips() const; + inline std::optional<Trip> get_trip(const Id & trip_id) const; + inline void add_trip(const Trip & trip); + + inline Result read_stop_times(); + inline const StopTimes & get_stop_times() const; + inline StopTimes get_stop_times_for_stop(const Id & stop_id) const; + inline StopTimes get_stop_times_for_trip(const Id & trip_id, bool sort_by_sequence = true) const; + inline void add_stop_time(const StopTime & stop_time); + + inline Result read_calendar(); + inline const Calendar & get_calendar() const; + inline std::optional<CalendarItem> get_calendar(const Id & service_id) const; + inline void add_calendar_item(const CalendarItem & calendar_item); + + inline Result read_calendar_dates(); + inline const CalendarDates & get_calendar_dates() const; + inline CalendarDates get_calendar_dates(const Id & service_id, bool sort_by_date = true) const; + inline void add_calendar_date(const CalendarDate & calendar_date); + + inline Result read_fare_rules(); + inline const FareRules & get_fare_rules() const; + inline std::optional<FareRule> get_fare_rule(const Id & fare_id) const; + inline void add_fare_rule(const FareRule & fare_rule); + + inline Result read_shapes(); + inline const Shapes & get_shapes() const; + inline Shape get_shape(const Id & shape_id, bool sort_by_sequence = true) const; + inline void add_shape(const ShapePoint & shape); + + inline Result read_frequencies(); + inline const Frequencies & get_frequencies() const; + inline std::optional<Frequency> get_frequency(const Id & trip_id) const; + inline void add_frequency(const Frequency & frequency); + + inline Result read_transfers(); + inline const Transfers & get_transfers() const; + inline std::optional<Transfer> get_transfer(const Id & from_stop_id, const Id & to_stop_id) const; + inline void add_transfer(const Transfer & transfer); + + inline Result read_pathways(); + inline const Pathways & get_pathways() const; + inline std::optional<Pathway> get_pathway(const Id & pathway_id) const; + inline std::optional<Pathway> get_pathway(const Id & from_stop_id, const Id & to_stop_id) const; + inline void add_pathway(const Pathway & pathway); + + inline Result read_levels(); + inline const Levels & get_levels() const; + inline std::optional<Level> get_level(const Id & level_id) const; + inline void add_level(const Level & level); + + inline Result read_feed_info(); + inline FeedInfo get_feed_info() const; + inline void set_feed_info(const FeedInfo & feed_info); + + inline Result read_translations(); + inline const Translations & get_translations() const; + inline std::optional<Translation> get_translation(const TranslationTable & table_name) const; + inline void add_translation(const Translation & translation); + + inline Result read_attributions(); + inline const Attributions & get_attributions() const; + inline void add_attribution(const Attribution & attribution); + +private: + inline Result parse_csv(const std::string & filename, + const std::function<Result(const ParsedCsvRow & record)> & add_entity); + + inline Result add_agency(ParsedCsvRow const & row); + inline Result add_route(ParsedCsvRow const & row); + inline Result add_shape(ParsedCsvRow const & row); + inline Result add_trip(ParsedCsvRow const & row); + inline Result add_stop(ParsedCsvRow const & row); + inline Result add_stop_time(ParsedCsvRow const & row); + inline Result add_calendar_item(ParsedCsvRow const & row); + inline Result add_calendar_date(ParsedCsvRow const & row); + inline Result add_transfer(ParsedCsvRow const & row); + inline Result add_frequency(ParsedCsvRow const & row); + + std::string gtfs_directory; + + Agencies agencies; + Stops stops; + Routes routes; + Trips trips; + StopTimes stop_times; + + Calendar calendar; + CalendarDates calendar_dates; + FareRules fare_rules; + Shape shapes; + Frequencies frequencies; + Transfers transfers; + Pathways pathways; + Levels levels; + Translations translations; + Attributions attributions; + FeedInfo feed_info; +}; + +inline Feed::Feed(const std::string & gtfs_path) : gtfs_directory(gtfs_path) {} + +inline Result Feed::read_feed() +{ + if (!std::filesystem::exists(gtfs_directory)) + return {ResultCode::ERROR_INVALID_GTFS_PATH, "Invalid path " + gtfs_directory}; + + // Read required files + if (auto const res = read_agencies(); res.code != ResultCode::OK) + return res; + + if (auto const res = read_stops(); res.code != ResultCode::OK) + return res; + + if (auto const res = read_routes(); res.code != ResultCode::OK) + return res; + + if (auto const res = read_trips(); res.code != ResultCode::OK) + return res; + + if (auto const res = read_stop_times(); res.code != ResultCode::OK) + return res; + + // Conditionally required: + if (auto const res = read_calendar(); res.code != ResultCode::OK) + { + if (res != ResultCode::ERROR_FILE_ABSENT) + return res; + } + + if (auto const res = read_calendar_dates(); res.code != ResultCode::OK) + { + if (res != ResultCode::ERROR_FILE_ABSENT) + return res; + } + + // Optional files: + if (auto const res = read_shapes(); res.code != ResultCode::OK) + { + if (res != ResultCode::ERROR_FILE_ABSENT) + return res; + } + + if (auto const res = read_transfers(); res.code != ResultCode::OK) + { + if (res != ResultCode::ERROR_FILE_ABSENT) + return res; + } + + if (auto const res = read_frequencies(); res.code != ResultCode::OK) + { + if (res != ResultCode::ERROR_FILE_ABSENT) + return res; + } + + // TODO Read other conditionally optional and optional files + + return {ResultCode::OK, {}}; +} + +inline Result Feed::write_feed(const std::string & gtfs_path) const +{ + if (gtfs_path.empty()) + return {ResultCode::ERROR_INVALID_GTFS_PATH, "Empty output path for writing feed"}; + // TODO Write feed to csv files + return {}; +} + +inline std::string get_value_or_default(ParsedCsvRow const & container, const std::string & key, + const std::string & default_value = "") +{ + const auto it = container.find(key); + if (it == container.end()) + return default_value; + + return it->second; +} + +template <class T> +inline void set_field(T & field, ParsedCsvRow const & container, const std::string & key, + bool is_optional = true) +{ + const std::string key_str = get_value_or_default(container, key); + if (!key_str.empty() || !is_optional) + field = static_cast<T>(std::stoi(key_str)); +} + +inline bool set_fractional(double & field, ParsedCsvRow const & container, const std::string & key, + bool is_optional = true) +{ + const std::string key_str = get_value_or_default(container, key); + if (!key_str.empty() || !is_optional) + { + field = std::stod(key_str); + return true; + } + return false; +} + +// Throw if not valid WGS84 decimal degrees. +inline void check_coordinates(double latitude, double longitude) +{ + if (latitude < -90.0 || latitude > 90.0) + throw std::out_of_range("Latitude"); + + if (longitude < -180.0 || longitude > 180.0) + throw std::out_of_range("Longitude"); +} + +inline Result Feed::add_agency(ParsedCsvRow const & row) +{ + Agency agency; + + // Conditionally required id: + agency.agency_id = get_value_or_default(row, "agency_id"); + + // Required fields: + try + { + agency.agency_name = row.at("agency_name"); + agency.agency_url = row.at("agency_url"); + agency.agency_timezone = row.at("agency_timezone"); + } + catch (const std::out_of_range & ex) + { + return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; + } + + // Optional fields: + agency.agency_lang = get_value_or_default(row, "agency_lang"); + agency.agency_phone = get_value_or_default(row, "agency_phone"); + agency.agency_fare_url = get_value_or_default(row, "agency_fare_url"); + agency.agency_email = get_value_or_default(row, "agency_email"); + + agencies.push_back(agency); + return {ResultCode::OK, {}}; +} + +inline Result Feed::add_route(ParsedCsvRow const & row) +{ + Route route; + + try + { + // Required fields: + route.route_id = row.at("route_id"); + set_field(route.route_type, row, "route_type", false); + + // Optional: + set_field(route.route_sort_order, row, "route_sort_order"); + } + catch (const std::out_of_range & ex) + { + return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; + } + catch (const std::invalid_argument & ex) + { + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; + } + + // Conditionally required: + route.agency_id = get_value_or_default(row, "agency_id"); + + route.route_short_name = get_value_or_default(row, "route_short_name"); + route.route_long_name = get_value_or_default(row, "route_long_name"); + + if (route.route_short_name.empty() && route.route_long_name.empty()) + { + return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, + "'route_short_name' or 'route_long_name' must be specified"}; + } + + route.route_color = get_value_or_default(row, "route_color"); + route.route_text_color = get_value_or_default(row, "route_text_color"); + route.route_desc = get_value_or_default(row, "route_desc"); + route.route_url = get_value_or_default(row, "route_url"); + + routes.push_back(route); + + return {ResultCode::OK, {}}; +} + +inline Result Feed::add_shape(ParsedCsvRow const & row) +{ + ShapePoint point; + try + { + // Required: + point.shape_id = row.at("shape_id"); + point.shape_pt_sequence = std::stoi(row.at("shape_pt_sequence")); + + point.shape_pt_lon = std::stod(row.at("shape_pt_lon")); + point.shape_pt_lat = std::stod(row.at("shape_pt_lat")); + check_coordinates(point.shape_pt_lat, point.shape_pt_lon); + + // Optional: + set_fractional(point.shape_dist_traveled, row, "shape_dist_traveled"); + if (point.shape_dist_traveled < 0.0) + throw std::invalid_argument("Invalid shape_dist_traveled"); + } + catch (const std::out_of_range & ex) + { + return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; + } + catch (const std::invalid_argument & ex) + { + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; + } + + shapes.push_back(point); + return {ResultCode::OK, {}}; +} + +inline Result Feed::add_trip(ParsedCsvRow const & row) +{ + Trip trip; + try + { + // Required: + trip.route_id = row.at("route_id"); + trip.service_id = row.at("service_id"); + trip.trip_id = row.at("trip_id"); + + // Optional: + set_field(trip.direction_id, row, "direction_id"); + set_field(trip.wheelchair_accessible, row, "wheelchair_accessible"); + set_field(trip.bikes_allowed, row, "bikes_allowed"); + } + catch (const std::out_of_range & ex) + { + return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; + } + catch (const std::invalid_argument & ex) + { + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; + } + + // Optional: + trip.shape_id = get_value_or_default(row, "shape_id"); + trip.trip_headsign = get_value_or_default(row, "trip_headsign"); + trip.trip_short_name = get_value_or_default(row, "trip_short_name"); + trip.block_id = get_value_or_default(row, "block_id"); + + trips.push_back(trip); + return {ResultCode::OK, {}}; +} + +inline Result Feed::add_stop(ParsedCsvRow const & row) +{ + Stop stop; + + try + { + stop.stop_id = row.at("stop_id"); + + // Optional: + bool const set_lon = set_fractional(stop.stop_lon, row, "stop_lon"); + bool const set_lat = set_fractional(stop.stop_lat, row, "stop_lat"); + + if (!set_lon || !set_lat) + stop.coordinates_present = false; + } + catch (const std::out_of_range & ex) + { + return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; + } + catch (const std::invalid_argument & ex) + { + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; + } + + // Conditionally required: + stop.stop_name = get_value_or_default(row, "stop_name"); + stop.parent_station = get_value_or_default(row, "parent_station"); + stop.zone_id = get_value_or_default(row, "zone_id"); + + // Optional: + stop.stop_code = get_value_or_default(row, "stop_code"); + stop.stop_desc = get_value_or_default(row, "stop_desc"); + stop.stop_url = get_value_or_default(row, "stop_url"); + set_field(stop.location_type, row, "location_type"); + stop.stop_timezone = get_value_or_default(row, "stop_timezone"); + stop.wheelchair_boarding = get_value_or_default(row, "wheelchair_boarding"); + stop.level_id = get_value_or_default(row, "level_id"); + stop.platform_code = get_value_or_default(row, "platform_code"); + + stops.push_back(stop); + + return {ResultCode::OK, {}}; +} + +inline Result Feed::add_stop_time(ParsedCsvRow const & row) +{ + StopTime stop_time; + + try + { + // Required: + stop_time.trip_id = row.at("trip_id"); + stop_time.stop_id = row.at("stop_id"); + stop_time.stop_sequence = std::stoi(row.at("stop_sequence")); + + // Conditionally required: + stop_time.departure_time = Time(row.at("departure_time")); + stop_time.arrival_time = Time(row.at("arrival_time")); + + // Optional: + set_field(stop_time.pickup_type, row, "pickup_type"); + set_field(stop_time.drop_off_type, row, "drop_off_type"); + + set_fractional(stop_time.shape_dist_traveled, row, "shape_dist_traveled"); + if (stop_time.shape_dist_traveled < 0.0) + throw std::invalid_argument("Invalid shape_dist_traveled"); + + set_field(stop_time.timepoint, row, "timepoint"); + } + catch (const std::out_of_range & ex) + { + return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; + } + catch (const std::invalid_argument & ex) + { + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; + } + catch (const InvalidFieldFormat & ex) + { + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; + } + + // Optional: + stop_time.stop_headsign = get_value_or_default(row, "stop_headsign"); + + stop_times.push_back(stop_time); + return {ResultCode::OK, {}}; +} + +inline Result Feed::add_calendar_item(ParsedCsvRow const & row) +{ + CalendarItem calendar_item; + try + { + // Required fields: + calendar_item.service_id = row.at("service_id"); + + set_field(calendar_item.monday, row, "monday", false); + set_field(calendar_item.tuesday, row, "tuesday", false); + set_field(calendar_item.wednesday, row, "wednesday", false); + set_field(calendar_item.thursday, row, "thursday", false); + set_field(calendar_item.friday, row, "friday", false); + set_field(calendar_item.saturday, row, "saturday", false); + set_field(calendar_item.sunday, row, "sunday", false); + + calendar_item.start_date = Date(row.at("start_date")); + calendar_item.end_date = Date(row.at("end_date")); + } + catch (const std::out_of_range & ex) + { + return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; + } + catch (const std::invalid_argument & ex) + { + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; + } + catch (const InvalidFieldFormat & ex) + { + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; + } + + calendar.push_back(calendar_item); + return {ResultCode::OK, {}}; +} + +inline Result Feed::add_calendar_date(ParsedCsvRow const & row) +{ + CalendarDate calendar_date; + try + { + // Required fields: + calendar_date.service_id = row.at("service_id"); + + set_field(calendar_date.exception_type, row, "exception_type", false); + calendar_date.date = Date(row.at("date")); + } + catch (const std::out_of_range & ex) + { + return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; + } + catch (const std::invalid_argument & ex) + { + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; + } + catch (const InvalidFieldFormat & ex) + { + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; + } + + calendar_dates.push_back(calendar_date); + return {ResultCode::OK, {}}; +} + +inline Result Feed::add_transfer(ParsedCsvRow const & row) +{ + Transfer transfer; + try + { + // Required fields: + transfer.from_stop_id = row.at("from_stop_id"); + transfer.to_stop_id = row.at("to_stop_id"); + set_field(transfer.transfer_type, row, "transfer_type", false); + + // Optional: + set_field(transfer.min_transfer_time, row, "min_transfer_time"); + } + catch (const std::out_of_range & ex) + { + return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; + } + catch (const std::invalid_argument & ex) + { + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; + } + catch (const InvalidFieldFormat & ex) + { + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; + } + + transfers.push_back(transfer); + return {ResultCode::OK, {}}; +} + +inline Result Feed::add_frequency(ParsedCsvRow const & row) +{ + Frequency frequency; + try + { + // Required fields: + frequency.trip_id = row.at("trip_id"); + frequency.start_time = Time(row.at("start_time")); + frequency.end_time = Time(row.at("end_time")); + set_field(frequency.headway_secs, row, "headway_secs", false); + + // Optional: + set_field(frequency.exact_times, row, "exact_times"); + } + catch (const std::out_of_range & ex) + { + return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; + } + catch (const std::invalid_argument & ex) + { + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; + } + catch (const InvalidFieldFormat & ex) + { + return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; + } + + frequencies.push_back(frequency); + return {ResultCode::OK, {}}; +} + +inline Result Feed::parse_csv(const std::string & filename, + const std::function<Result(const ParsedCsvRow & record)> & add_entity) +{ + CsvParser parser(gtfs_directory); + auto res_header = parser.read_header(filename); + if (res_header.code != ResultCode::OK) + return res_header; + + ParsedCsvRow record; + Result res_row; + while ((res_row = parser.read_row(record)) != ResultCode::END_OF_FILE) + { + if (res_row != ResultCode::OK) + return res_row; + + if (record.empty()) + continue; + + Result res = add_entity(record); + if (res != ResultCode::OK) + return res; + } + + return {ResultCode::OK, {"Parsed " + filename}}; +} + +inline Result Feed::read_agencies() +{ + auto handler = [this](const ParsedCsvRow & record) { return this->add_agency(record); }; + return parse_csv("agency.txt", handler); +} + +inline const Agencies & Feed::get_agencies() const { return agencies; } + +inline std::optional<Agency> Feed::get_agency(const Id & agency_id) const +{ + // agency id is required when the dataset contains data for multiple agencies, + // otherwise it is optional: + if (agency_id.empty() && agencies.size() == 1) + return agencies[0]; + + const auto it = + std::find_if(agencies.begin(), agencies.end(), + [&agency_id](const Agency & agency) { return agency.agency_id == agency_id; }); + + if (it == agencies.end()) + return std::nullopt; + + return *it; +} + +inline void Feed::add_agency(const Agency & agency) { agencies.push_back(agency); } + +inline Result Feed::read_stops() +{ + auto handler = [this](const ParsedCsvRow & record) { return this->add_stop(record); }; + return parse_csv("stops.txt", handler); +} + +inline const Stops & Feed::get_stops() const { return stops; } + +inline std::optional<Stop> Feed::get_stop(const Id & stop_id) const +{ + const auto it = std::find_if(stops.begin(), stops.end(), + [&stop_id](const Stop & stop) { return stop.stop_id == stop_id; }); + + if (it == stops.end()) + return std::nullopt; + + return *it; +} + +inline void Feed::add_stop(const Stop & stop) { stops.push_back(stop); } + +inline Result Feed::read_routes() +{ + auto handler = [this](const ParsedCsvRow & record) { return this->add_route(record); }; + return parse_csv("routes.txt", handler); +} + +inline const Routes & Feed::get_routes() const { return routes; } + +inline std::optional<Route> Feed::get_route(const Id & route_id) const +{ + const auto it = std::find_if(routes.begin(), routes.end(), [&route_id](const Route & route) { + return route.route_id == route_id; + }); + + if (it == routes.end()) + return std::nullopt; + + return *it; +} + +inline void Feed::add_route(const Route & route) { routes.push_back(route); } + +inline Result Feed::read_trips() +{ + auto handler = [this](const ParsedCsvRow & record) { return this->add_trip(record); }; + return parse_csv("trips.txt", handler); +} + +inline const Trips & Feed::get_trips() const { return trips; } + +inline std::optional<Trip> Feed::get_trip(const Id & trip_id) const +{ + const auto it = std::find_if(trips.begin(), trips.end(), + [&trip_id](const Trip & trip) { return trip.trip_id == trip_id; }); + + if (it == trips.end()) + return std::nullopt; + + return *it; +} + +inline void Feed::add_trip(const Trip & trip) { trips.push_back(trip); } + +inline Result Feed::read_stop_times() +{ + auto handler = [this](const ParsedCsvRow & record) { return this->add_stop_time(record); }; + return parse_csv("stop_times.txt", handler); +} + +inline const StopTimes & Feed::get_stop_times() const { return stop_times; } + +inline StopTimes Feed::get_stop_times_for_stop(const Id & stop_id) const +{ + StopTimes res; + for (const auto & stop_time : stop_times) + { + if (stop_time.stop_id == stop_id) + res.push_back(stop_time); + } + return res; +} + +inline StopTimes Feed::get_stop_times_for_trip(const Id & trip_id, bool sort_by_sequence) const +{ + StopTimes res; + for (const auto & stop_time : stop_times) + { + if (stop_time.trip_id == trip_id) + res.push_back(stop_time); + } + if (sort_by_sequence) + { + std::sort(res.begin(), res.end(), [](const StopTime & t1, const StopTime & t2) { + return t1.stop_sequence < t2.stop_sequence; + }); + } + return res; +} + +inline void Feed::add_stop_time(const StopTime & stop_time) { stop_times.push_back(stop_time); } + +inline Result Feed::read_calendar() +{ + auto handler = [this](const ParsedCsvRow & record) { return this->add_calendar_item(record); }; + return parse_csv("calendar.txt", handler); +} + +inline const Calendar & Feed::get_calendar() const { return calendar; } + +inline std::optional<CalendarItem> Feed::get_calendar(const Id & service_id) const +{ + const auto it = std::find_if(calendar.begin(), calendar.end(), + [&service_id](const CalendarItem & calendar_item) { + return calendar_item.service_id == service_id; + }); + + if (it == calendar.end()) + return std::nullopt; + + return *it; +} + +inline void Feed::add_calendar_item(const CalendarItem & calendar_item) +{ + calendar.push_back(calendar_item); +} + +inline Result Feed::read_calendar_dates() +{ + auto handler = [this](const ParsedCsvRow & record) { return this->add_calendar_date(record); }; + return parse_csv("calendar_dates.txt", handler); +} + +inline const CalendarDates & Feed::get_calendar_dates() const { return calendar_dates; } + +inline CalendarDates Feed::get_calendar_dates(const Id & service_id, bool sort_by_date) const +{ + std::vector<CalendarDate> res; + for (const auto & calendar_date : calendar_dates) + { + if (calendar_date.service_id == service_id) + res.push_back(calendar_date); + } + + if (sort_by_date) + { + std::sort(res.begin(), res.end(), [](const CalendarDate & d1, const CalendarDate & d2) { + return d1.date.get_raw_date() < d2.date.get_raw_date(); + }); + } + + return res; +} + +inline void Feed::add_calendar_date(const CalendarDate & calendar_date) +{ + calendar_dates.push_back(calendar_date); +} + +inline Result Feed::read_fare_rules() +{ + // TODO Read csv + return {}; +} + +inline const FareRules & Feed::get_fare_rules() const { return fare_rules; } + +inline std::optional<FareRule> Feed::get_fare_rule(const Id & fare_id) const +{ + const auto it = + std::find_if(fare_rules.begin(), fare_rules.end(), + [&fare_id](const FareRule & fare_rule) { return fare_rule.fare_id == fare_id; }); + + if (it == fare_rules.end()) + return std::nullopt; + + return *it; +} + +inline void Feed::add_fare_rule(const FareRule & fare_rule) { fare_rules.push_back(fare_rule); } + +inline Result Feed::read_shapes() +{ + auto handler = [this](const ParsedCsvRow & record) { return this->add_shape(record); }; + return parse_csv("shapes.txt", handler); +} + +inline const Shapes & Feed::get_shapes() const { return shapes; } + +inline Shape Feed::get_shape(const Id & shape_id, bool sort_by_sequence) const +{ + Shape res; + for (const auto & shape : shapes) + { + if (shape.shape_id == shape_id) + res.push_back(shape); + } + if (sort_by_sequence) + { + std::sort(res.begin(), res.end(), [](const ShapePoint & s1, const ShapePoint & s2) { + return s1.shape_pt_sequence < s2.shape_pt_sequence; + }); + } + return res; +} + +inline void Feed::add_shape(const ShapePoint & shape) { shapes.push_back(shape); } + +inline Result Feed::read_frequencies() +{ + auto handler = [this](const ParsedCsvRow & record) { return this->add_frequency(record); }; + return parse_csv("frequencies.txt", handler); +} + +inline const Frequencies & Feed::get_frequencies() const { return frequencies; } + +inline std::optional<Frequency> Feed::get_frequency(const Id & trip_id) const +{ + const auto it = std::find_if( + frequencies.begin(), frequencies.end(), + [&trip_id](const Frequency & frequency) { return frequency.trip_id == trip_id; }); + + if (it == frequencies.end()) + return std::nullopt; + + return *it; +} + +inline void Feed::add_frequency(const Frequency & frequency) { frequencies.push_back(frequency); } + +inline Result Feed::read_transfers() +{ + auto handler = [this](const ParsedCsvRow & record) { return this->add_transfer(record); }; + return parse_csv("transfers.txt", handler); +} + +inline const Transfers & Feed::get_transfers() const { return transfers; } + +inline std::optional<Transfer> Feed::get_transfer(const Id & from_stop_id, + const Id & to_stop_id) const +{ + const auto it = std::find_if( + transfers.begin(), transfers.end(), [&from_stop_id, &to_stop_id](const Transfer & transfer) { + return transfer.from_stop_id == from_stop_id && transfer.to_stop_id == to_stop_id; + }); + + if (it == transfers.end()) + return std::nullopt; + + return *it; +} + +inline void Feed::add_transfer(const Transfer & transfer) { transfers.push_back(transfer); } + +inline Result Feed::read_pathways() +{ + // TODO Read csv + return {}; +} + +inline const Pathways & Feed::get_pathways() const { return pathways; } + +inline std::optional<Pathway> Feed::get_pathway(const Id & pathway_id) const +{ + const auto it = std::find_if( + pathways.begin(), pathways.end(), + [&pathway_id](const Pathway & pathway) { return pathway.pathway_d == pathway_id; }); + + if (it == pathways.end()) + return std::nullopt; + + return *it; +} + +inline std::optional<Pathway> Feed::get_pathway(const Id & from_stop_id, + const Id & to_stop_id) const +{ + const auto it = std::find_if( + pathways.begin(), pathways.end(), [&from_stop_id, &to_stop_id](const Pathway & pathway) { + return pathway.from_stop_id == from_stop_id && pathway.to_stop_id == to_stop_id; + }); + + if (it == pathways.end()) + return std::nullopt; + + return *it; +} + +inline void Feed::add_pathway(const Pathway & pathway) { pathways.push_back(pathway); } + +inline Result Feed::read_levels() +{ + // TODO Read csv + return {}; +} + +inline const Levels & Feed::get_levels() const { return levels; } + +inline std::optional<Level> Feed::get_level(const Id & level_id) const +{ + const auto it = std::find_if(levels.begin(), levels.end(), [&level_id](const Level & level) { + return level.level_id == level_id; + }); + + if (it == levels.end()) + return std::nullopt; + + return *it; +} + +inline void Feed::add_level(const Level & level) { levels.push_back(level); } + +inline Result Feed::read_feed_info() +{ + // TODO Read csv + return {}; +} + +inline FeedInfo Feed::get_feed_info() const { return feed_info; } + +inline void Feed::set_feed_info(const FeedInfo & info) { feed_info = info; } + +inline Result Feed::read_translations() +{ + // TODO Read csv + return {}; +} + +inline const Translations & Feed::get_translations() const { return translations; } + +inline std::optional<Translation> Feed::get_translation(const TranslationTable & table_name) const +{ + const auto it = std::find_if(translations.begin(), translations.end(), + [&table_name](const Translation & translation) { + return translation.table_name == table_name; + }); + + if (it == translations.end()) + return std::nullopt; + + return *it; +} + +inline void Feed::add_translation(const Translation & translation) +{ + translations.push_back(translation); +} + +inline Result Feed::read_attributions() +{ + // TODO Read csv + return {}; +} + +inline const Attributions & Feed::get_attributions() const { return attributions; } + +inline void Feed::add_attribution(const Attribution & attribution) +{ + attributions.push_back(attribution); +} +} // namespace gtfs diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..aadc7f2 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,10 @@ +file(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.cpp) + +message(STATUS "CMAKE_CURRENT_BINARY_DIR=" ${CMAKE_CURRENT_BINARY_DIR}) + +foreach(TEST_SOURCE ${TESTS}) + string(REPLACE ".cpp" "" TEST_TARGET "${TEST_SOURCE}") + add_executable(${TEST_TARGET} ${TEST_SOURCE}) + target_compile_features(${TEST_TARGET} PRIVATE cxx_std_17) + add_test("${TEST_TARGET}" "${TEST_TARGET}" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} --verbose) +endforeach() diff --git a/tests/data/sample_feed/agency.txt b/tests/data/sample_feed/agency.txt new file mode 100644 index 0000000..eb24555 --- /dev/null +++ b/tests/data/sample_feed/agency.txt @@ -0,0 +1,2 @@ +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,http://google.com,America/Los_Angeles \ No newline at end of file diff --git a/tests/data/sample_feed/calendar.txt b/tests/data/sample_feed/calendar.txt new file mode 100644 index 0000000..7a2abb5 --- /dev/null +++ b/tests/data/sample_feed/calendar.txt @@ -0,0 +1,3 @@ +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +FULLW,1,1,1,1,1,1,1,20070101,20101231 +WE,0,0,0,0,0,1,1,20070101,20101231 \ No newline at end of file diff --git a/tests/data/sample_feed/calendar_dates.txt b/tests/data/sample_feed/calendar_dates.txt new file mode 100644 index 0000000..94a557b --- /dev/null +++ b/tests/data/sample_feed/calendar_dates.txt @@ -0,0 +1,2 @@ +service_id,date,exception_type +FULLW,20070604,2 \ No newline at end of file diff --git a/tests/data/sample_feed/fare_attributes.txt b/tests/data/sample_feed/fare_attributes.txt new file mode 100644 index 0000000..3ee7a99 --- /dev/null +++ b/tests/data/sample_feed/fare_attributes.txt @@ -0,0 +1,3 @@ +fare_id,price,currency_type,payment_method,transfers,transfer_duration +p,1.25,USD,0,0, +a,5.25,USD,0,0, \ No newline at end of file diff --git a/tests/data/sample_feed/fare_rules.txt b/tests/data/sample_feed/fare_rules.txt new file mode 100644 index 0000000..ee776c9 --- /dev/null +++ b/tests/data/sample_feed/fare_rules.txt @@ -0,0 +1,5 @@ +fare_id,route_id,origin_id,destination_id,contains_id +p,AB,,, +p,STBA,,, +p,BFC,,, +a,AAMV,,, \ No newline at end of file diff --git a/tests/data/sample_feed/frequencies.txt b/tests/data/sample_feed/frequencies.txt new file mode 100644 index 0000000..47941ef --- /dev/null +++ b/tests/data/sample_feed/frequencies.txt @@ -0,0 +1,12 @@ +trip_id,start_time,end_time,headway_secs +STBA,6:00:00,22:00:00,1800 +CITY1,6:00:00,7:59:59,1800 +CITY2,6:00:00,7:59:59,1800 +CITY1,8:00:00,9:59:59,600 +CITY2,8:00:00,9:59:59,600 +CITY1,10:00:00,15:59:59,1800 +CITY2,10:00:00,15:59:59,1800 +CITY1,16:00:00,18:59:59,600 +CITY2,16:00:00,18:59:59,600 +CITY1,19:00:00,22:00:00,1800 +CITY2,19:00:00,22:00:00,1800 \ No newline at end of file diff --git a/tests/data/sample_feed/routes.txt b/tests/data/sample_feed/routes.txt new file mode 100644 index 0000000..0b1eb61 --- /dev/null +++ b/tests/data/sample_feed/routes.txt @@ -0,0 +1,6 @@ +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type,route_url,route_color,route_text_color +AB,DTA,10,Airport - Bullfrog,,3,,, +BFC,DTA,20,Bullfrog - Furnace Creek Resort,,3,,, +STBA,DTA,30,Stagecoach - Airport Shuttle,,3,,, +CITY,DTA,40,City,,3,,, +AAMV,DTA,50,Airport - Amargosa Valley,,3,,, \ No newline at end of file diff --git a/tests/data/sample_feed/shapes.txt b/tests/data/sample_feed/shapes.txt new file mode 100644 index 0000000..70956bf --- /dev/null +++ b/tests/data/sample_feed/shapes.txt @@ -0,0 +1,9 @@ +shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence,shape_dist_traveled +10237, 43.5176524709, -79.6906570431,50017,12669 +10237, 43.5176982107, -79.6906412064,50018,12669 +10237, 43.5177439788, -79.6906278437,50019,12669 +10237, 43.5177457792, -79.6906278048,50020,12669 +10243, 43.6448714082, -79.5249161004,10001,0 +10243, 43.6448078510, -79.5252239093,10002,0 +10243, 43.6446766156, -79.5251713255,10003,0 +10243, 43.6445544452, -79.5251234796,10004,0 \ No newline at end of file diff --git a/tests/data/sample_feed/stop_times.txt b/tests/data/sample_feed/stop_times.txt new file mode 100644 index 0000000..89cf487 --- /dev/null +++ b/tests/data/sample_feed/stop_times.txt @@ -0,0 +1,29 @@ +trip_id,arrival_time,departure_time,stop_id,stop_sequence,stop_headsign,pickup_type,drop_off_time,shape_dist_traveled +STBA,6:00:00,6:00:00,STAGECOACH,1,,,, +STBA,6:20:00,6:20:00,BEATTY_AIRPORT,2,,,, +CITY1,6:00:00,6:00:00,STAGECOACH,1,,,, +CITY1,6:05:00,6:07:00,NANAA,2,,,, +CITY1,6:12:00,6:14:00,NADAV,3,,,, +CITY1,6:19:00,6:21:00,DADAN,4,,,, +CITY1,6:26:00,6:28:00,EMSI,5,,,, +CITY2,6:28:00,6:30:00,EMSI,1,,,, +CITY2,6:35:00,6:37:00,DADAN,2,,,, +CITY2,6:42:00,6:44:00,NADAV,3,,,, +CITY2,6:49:00,6:51:00,NANAA,4,,,, +CITY2,6:56:00,6:58:00,STAGECOACH,5,,,, +AB1,8:00:00,8:00:00,BEATTY_AIRPORT,1,,,, +AB1,8:10:00,8:15:00,BULLFROG,2,,,, +AB2,12:05:00,12:05:00,BULLFROG,1,,,, +AB2,12:15:00,12:15:00,BEATTY_AIRPORT,2 +BFC1,8:20:00,8:20:00,BULLFROG,1 +BFC1,9:20:00,9:20:00,FUR_CREEK_RES,2 +BFC2,11:00:00,11:00:00,FUR_CREEK_RES,1 +BFC2,12:00:00,12:00:00,BULLFROG,2 +AAMV1,8:00:00,8:00:00,BEATTY_AIRPORT,1 +AAMV1,9:00:00,9:00:00,AMV,2 +AAMV2,10:00:00,10:00:00,AMV,1 +AAMV2,11:00:00,11:00:00,BEATTY_AIRPORT,2 +AAMV3,13:00:00,13:00:00,BEATTY_AIRPORT,1 +AAMV3,14:00:00,14:00:00,AMV,2 +AAMV4,15:00:00,15:00:00,AMV,1 +AAMV4,16:00:00,16:00:00,BEATTY_AIRPORT,2 \ No newline at end of file diff --git a/tests/data/sample_feed/stops.txt b/tests/data/sample_feed/stops.txt new file mode 100644 index 0000000..4ee756a --- /dev/null +++ b/tests/data/sample_feed/stops.txt @@ -0,0 +1,10 @@ +stop_id,stop_name,stop_desc,stop_lat,stop_lon,zone_id,stop_url +FUR_CREEK_RES,Furnace Creek Resort (Demo),,36.425288,-117.133162,, +BEATTY_AIRPORT,Nye County Airport (Demo),,36.868446,-116.784582,, +BULLFROG,Bullfrog (Demo),,36.88108,-116.81797,, +STAGECOACH,Stagecoach Hotel & Casino (Demo),,36.915682,-116.751677,, +NADAV,North Ave / D Ave N (Demo),,36.914893,-116.76821,, +NANAA,North Ave / N A Ave (Demo),,36.914944,-116.761472,, +DADAN,Doing Ave / D Ave N (Demo),,36.909489,-116.768242,, +EMSI,E Main St / S Irving St (Demo),,36.905697,-116.76218,, +AMV,Amargosa Valley (Demo),,36.641496,-116.40094,, \ No newline at end of file diff --git a/tests/data/sample_feed/trips.txt b/tests/data/sample_feed/trips.txt new file mode 100644 index 0000000..41aad6e --- /dev/null +++ b/tests/data/sample_feed/trips.txt @@ -0,0 +1,12 @@ +route_id,service_id,trip_id,trip_headsign,direction_id,block_id,shape_id +AB,FULLW,AB1,to Bullfrog,0,1, +AB,FULLW,AB2,to Airport,1,2, +STBA,FULLW,STBA,Shuttle,,, +CITY,FULLW,CITY1,,0,, +CITY,FULLW,CITY2,,1,, +BFC,FULLW,BFC1,to Furnace Creek Resort,0,1, +BFC,FULLW,BFC2,to Bullfrog,1,2, +AAMV,WE,AAMV1,to Amargosa Valley,0,, +AAMV,WE,AAMV2,to Airport,1,, +AAMV,WE,AAMV3,to Amargosa Valley,0,, +AAMV,WE,AAMV4,to Airport,1,, \ No newline at end of file diff --git a/tests/unit_tests.cpp b/tests/unit_tests.cpp new file mode 100644 index 0000000..629974a --- /dev/null +++ b/tests/unit_tests.cpp @@ -0,0 +1,345 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest.h" + +#include "just_gtfs/just_gtfs.h" + +using namespace gtfs; + +TEST_SUITE_BEGIN("Handling time GTFS fields"); +TEST_CASE("Time in H:MM:SS format") +{ + Time stop_time("0:19:00"); + CHECK(stop_time.is_provided()); + CHECK_EQ(stop_time.get_hh_mm_ss(), std::make_tuple(0, 19, 0)); + CHECK_EQ(stop_time.get_raw_time(), "0:19:00"); + CHECK_EQ(stop_time.get_total_seconds(), 19 * 60); +} + +TEST_CASE("Time in HH:MM:SS format") +{ + Time stop_time("39:45:30"); + CHECK_EQ(stop_time.get_hh_mm_ss(), std::make_tuple(39, 45, 30)); + CHECK_EQ(stop_time.get_raw_time(), "39:45:30"); + CHECK_EQ(stop_time.get_total_seconds(), 39 * 60 * 60 + 45 * 60 + 30); +} + +TEST_CASE("Time from integers 1") +{ + Time stop_time(14, 30, 0); + CHECK_EQ(stop_time.get_hh_mm_ss(), std::make_tuple(14, 30, 0)); + CHECK_EQ(stop_time.get_raw_time(), "14:30:00"); + CHECK_EQ(stop_time.get_total_seconds(), 14 * 60 * 60 + 30 * 60); +} + +TEST_CASE("Time from integers 2") +{ + Time stop_time(3, 0, 0); + CHECK_EQ(stop_time.get_hh_mm_ss(), std::make_tuple(3, 0, 0)); + CHECK_EQ(stop_time.get_raw_time(), "03:00:00"); + CHECK_EQ(stop_time.get_total_seconds(), 3 * 60 * 60); +} + +TEST_CASE("Invalid time format") +{ + CHECK_THROWS_AS(Time("12/10/00"), const InvalidFieldFormat &); + CHECK_THROWS_AS(Time("12:100:00"), const InvalidFieldFormat &); + CHECK_THROWS_AS(Time("12:10:100"), const InvalidFieldFormat &); +} + +TEST_CASE("Time not provided") +{ + Time stop_time(""); + CHECK(!stop_time.is_provided()); +} + +TEST_CASE("Convert to Time with 24 hours max") +{ + Time stop_time_near_midnight("24:05:00"); + CHECK(stop_time_near_midnight.limit_hours_to_24max()); + CHECK_EQ(stop_time_near_midnight.get_raw_time(), "00:05:00"); + + Time stop_time_morning("27:05:00"); + stop_time_morning.limit_hours_to_24max(); + CHECK_EQ(stop_time_morning.get_raw_time(), "03:05:00"); +} + +TEST_SUITE_END(); + +TEST_SUITE_BEGIN("Handling date GTFS fields"); +TEST_CASE("Date not provided") +{ + Date date(""); + CHECK(!date.is_provided()); +} + +TEST_CASE("Invalid date format") +{ + // Violation of the format YYYYMMDD: + CHECK_THROWS_AS(Date("1999314"), const InvalidFieldFormat &); + CHECK_THROWS_AS(Date("20081414"), const InvalidFieldFormat &); + CHECK_THROWS_AS(Date("20170432"), const InvalidFieldFormat &); + + // Count of days in february (leap year): + CHECK_THROWS_AS(Date("20200230"), const InvalidFieldFormat &); + // Count of days in february (not leap year): + CHECK_THROWS_AS(Date("20210229"), const InvalidFieldFormat &); + + // Count of days in months with 30 days: + CHECK_THROWS_AS(Date("19980431"), const InvalidFieldFormat &); + CHECK_THROWS_AS(Date("19980631"), const InvalidFieldFormat &); + CHECK_THROWS_AS(Date("19980931"), const InvalidFieldFormat &); + CHECK_THROWS_AS(Date("19981131"), const InvalidFieldFormat &); +} + +TEST_CASE("Date from string 1") +{ + Date date("20230903"); + CHECK_EQ(date.get_yyyy_mm_dd(), std::make_tuple(2023, 9, 3)); + CHECK_EQ(date.get_raw_date(), "20230903"); + CHECK(date.is_provided()); +} + +TEST_CASE("Date from string 2") +{ + Date date("20161231"); + CHECK_EQ(date.get_yyyy_mm_dd(), std::make_tuple(2016, 12, 31)); + CHECK_EQ(date.get_raw_date(), "20161231"); + CHECK(date.is_provided()); +} + +TEST_CASE("Date from string 3") +{ + Date date("20200229"); + CHECK_EQ(date.get_yyyy_mm_dd(), std::make_tuple(2020, 2, 29)); + CHECK_EQ(date.get_raw_date(), "20200229"); + CHECK(date.is_provided()); +} + +TEST_CASE("Date from integers") +{ + Date date(2022, 8, 16); + CHECK_EQ(date.get_yyyy_mm_dd(), std::make_tuple(2022, 8, 16)); + + CHECK_EQ(date.get_raw_date(), "20220816"); + CHECK(date.is_provided()); +} + +TEST_SUITE_END(); + +TEST_SUITE_BEGIN("Csv parsing"); +TEST_CASE("Record with empty values") +{ + const auto res = CsvParser::split_record(",, ,"); + CHECK_EQ(res.size(), 4); + for (const auto & token : res) + CHECK(token.empty()); +} + +TEST_CASE("Header with UTF BOM") +{ + const auto res = CsvParser::split_record("\xef\xbb\xbfroute_id, agency_id", true); + CHECK_EQ(res.size(), 2); + CHECK_EQ(res[0], "route_id"); + CHECK_EQ(res[1], "agency_id"); +} + +TEST_CASE("Quotation marks") +{ + const auto res = CsvParser::split_record(R"(27681 ,,"Sisters, OR",,"44.29124",1)"); + CHECK_EQ(res.size(), 6); + CHECK_EQ(res[2], "Sisters, OR"); + CHECK_EQ(res[4], "44.29124"); + CHECK_EQ(res[5], "1"); +} +TEST_SUITE_END(); + +TEST_SUITE_BEGIN("Read"); +// Credits: +// https://www.sfmta.com/reports/gtfs-transit-data +TEST_CASE("Empty container before parsing") +{ + Feed feed("data/San Francisco Municipal Transportation Agency"); + CHECK(feed.get_agencies().empty()); + auto agency = feed.get_agency("10"); + CHECK(!agency); +} + +TEST_CASE("Transfers") +{ + Feed feed("data/sample_feed"); + auto res = feed.read_transfers(); + CHECK_EQ(res.code, ResultCode::ERROR_FILE_ABSENT); + CHECK_EQ(feed.get_transfers().size(), 0); +} + +TEST_CASE("Calendar") +{ + Feed feed("data/sample_feed"); + auto res = feed.read_calendar(); + CHECK_EQ(res.code, ResultCode::OK); + const auto & calendar = feed.get_calendar(); + CHECK_EQ(calendar.size(), 2); + + const auto calendar_record = feed.get_calendar("WE"); + CHECK(calendar_record); + + CHECK_EQ(calendar_record->start_date, Date(2007, 01, 01)); + CHECK_EQ(calendar_record->end_date, Date(2010, 12, 31)); + + CHECK_EQ(calendar_record->monday, CalendarAvailability::NotAvailable); + CHECK_EQ(calendar_record->tuesday, CalendarAvailability::NotAvailable); + CHECK_EQ(calendar_record->wednesday, CalendarAvailability::NotAvailable); + CHECK_EQ(calendar_record->thursday, CalendarAvailability::NotAvailable); + CHECK_EQ(calendar_record->friday, CalendarAvailability::NotAvailable); + CHECK_EQ(calendar_record->saturday, CalendarAvailability::Available); + CHECK_EQ(calendar_record->sunday, CalendarAvailability::Available); +} + +TEST_CASE("Calendar dates") +{ + Feed feed("data/sample_feed"); + auto res = feed.read_calendar_dates(); + CHECK_EQ(res.code, ResultCode::OK); + const auto & calendar_dates = feed.get_calendar_dates(); + CHECK_EQ(calendar_dates.size(), 1); + + const auto calendar_record = feed.get_calendar_dates("FULLW"); + CHECK(!calendar_record.empty()); + + CHECK_EQ(calendar_record[0].date, Date(2007, 06, 04)); + CHECK_EQ(calendar_record[0].exception_type, CalendarDateException::Removed); +} + +TEST_CASE("Read GTFS feed") +{ + Feed feed("data/sample_feed"); + auto res = feed.read_feed(); + CHECK_EQ(res.code, ResultCode::OK); + CHECK_EQ(feed.get_agencies().size(), 1); + CHECK_EQ(feed.get_routes().size(), 5); + CHECK_EQ(feed.get_trips().size(), 11); + CHECK_EQ(feed.get_shapes().size(), 8); + CHECK_EQ(feed.get_stops().size(), 9); + CHECK_EQ(feed.get_stop_times().size(), 28); +} + +TEST_CASE("Agencies") +{ + Feed feed("data/sample_feed"); + auto res = feed.read_agencies(); + CHECK_EQ(res.code, ResultCode::OK); + const auto & agencies = feed.get_agencies(); + CHECK_EQ(agencies.size(), 1); + CHECK_EQ(agencies[0].agency_id, "DTA"); + CHECK_EQ(agencies[0].agency_name, "Demo Transit Authority"); + CHECK_EQ(agencies[0].agency_url, "http://google.com"); + CHECK(agencies[0].agency_lang.empty()); + CHECK_EQ(agencies[0].agency_timezone, "America/Los_Angeles"); + + const auto agency = feed.get_agency("DTA"); + CHECK(agency); +} + +TEST_CASE("Routes") +{ + Feed feed("data/sample_feed"); + auto res = feed.read_routes(); + CHECK_EQ(res.code, ResultCode::OK); + const auto & routes = feed.get_routes(); + CHECK_EQ(routes.size(), 5); + CHECK_EQ(routes[0].route_id, "AB"); + CHECK_EQ(routes[0].agency_id, "DTA"); + CHECK_EQ(routes[0].route_short_name, "10"); + CHECK_EQ(routes[0].route_long_name, "Airport - Bullfrog"); + CHECK_EQ(routes[0].route_type, RouteType::Bus); + CHECK(routes[0].route_text_color.empty()); + CHECK(routes[0].route_color.empty()); + CHECK(routes[0].route_desc.empty()); + + auto const route = feed.get_route("AB"); + CHECK(route); +} + +TEST_CASE("Trips") +{ + Feed feed("data/sample_feed"); + auto res = feed.read_trips(); + CHECK_EQ(res.code, ResultCode::OK); + const auto & trips = feed.get_trips(); + CHECK_EQ(trips.size(), 11); + + CHECK_EQ(trips[0].block_id, "1"); + CHECK_EQ(trips[0].route_id, "AB"); + CHECK_EQ(trips[0].direction_id, TripDirectionId::DefaultDirection); + CHECK_EQ(trips[0].trip_headsign, "to Bullfrog"); + CHECK(trips[0].shape_id.empty()); + CHECK_EQ(trips[0].service_id, "FULLW"); + CHECK_EQ(trips[0].trip_id, "AB1"); + + auto const trip = feed.get_trip("AB1"); + CHECK(trip); + CHECK(trip.value().trip_short_name.empty()); +} + +TEST_CASE("Stops") +{ + Feed feed("data/sample_feed"); + auto res = feed.read_stops(); + CHECK_EQ(res.code, ResultCode::OK); + + const auto & stops = feed.get_stops(); + CHECK_EQ(stops.size(), 9); + CHECK_EQ(stops[0].stop_lat, 36.425288); + CHECK_EQ(stops[0].stop_lon, -117.133162); + CHECK(stops[0].stop_code.empty()); + CHECK(stops[0].stop_url.empty()); + CHECK_EQ(stops[0].stop_id, "FUR_CREEK_RES"); + CHECK(stops[0].stop_desc.empty()); + CHECK_EQ(stops[0].stop_name, "Furnace Creek Resort (Demo)"); + CHECK_EQ(stops[0].location_type, StopLocationType::GenericNode); + CHECK(stops[0].zone_id.empty()); + + auto const stop = feed.get_stop("FUR_CREEK_RES"); + CHECK(stop); +} + +TEST_CASE("StopTimes") +{ + Feed feed("data/sample_feed"); + auto res = feed.read_stop_times(); + CHECK_EQ(res.code, ResultCode::OK); + + const auto & stop_times = feed.get_stop_times(); + CHECK_EQ(stop_times.size(), 28); + + CHECK_EQ(stop_times[0].trip_id, "STBA"); + CHECK_EQ(stop_times[0].arrival_time, Time(06, 00, 00)); + CHECK_EQ(stop_times[0].departure_time, Time(06, 00, 00)); + CHECK_EQ(stop_times[0].stop_id, "STAGECOACH"); + CHECK_EQ(stop_times[0].stop_sequence, 1); + CHECK(stop_times[0].stop_headsign.empty()); + CHECK_EQ(stop_times[0].pickup_type, StopTimeBoarding::RegularlyScheduled); + CHECK_EQ(stop_times[0].drop_off_type, StopTimeBoarding::RegularlyScheduled); + + CHECK_EQ(feed.get_stop_times_for_stop("STAGECOACH").size(), 3); + CHECK_EQ(feed.get_stop_times_for_trip("STBA").size(), 2); +} + +TEST_CASE("Shapes") +{ + Feed feed("data/sample_feed"); + auto res = feed.read_shapes(); + CHECK_EQ(res.code, ResultCode::OK); + + const auto & shapes = feed.get_shapes(); + CHECK_EQ(shapes.size(), 8); + CHECK_EQ(shapes[0].shape_id, "10237"); + CHECK_EQ(shapes[0].shape_pt_lat, 43.5176524709); + CHECK_EQ(shapes[0].shape_pt_lon, -79.6906570431); + CHECK_EQ(shapes[0].shape_pt_sequence, 50017); + CHECK_EQ(shapes[0].shape_dist_traveled, 12669); + + auto const shape = feed.get_shape("10237"); + CHECK_EQ(shape.size(), 4); +} +TEST_SUITE_END();