Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mapsme/just_gtfs.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormesozoic-drones <mesozoic.drones@gmail.com>2020-03-28 12:05:20 +0300
committerOlga Khlopkova <o.khlopkova@corp.mail.ru>2020-04-27 15:59:07 +0300
commit083d98e4315574a4d556d3fc8184c71ae1234ef9 (patch)
tree0e7f8d0968997ae67c55c3203872b0a42c32b79b
parentc8c9a5d2b491f1868fe8deda4e8db58b7a394d68 (diff)
Update README.md
-rw-r--r--.clang-format31
-rw-r--r--.github/workflows/ccpp.yml31
-rw-r--r--.gitignore9
-rw-r--r--.gitmodules3
-rw-r--r--CMakeLists.txt15
-rw-r--r--LICENSE.MIT (renamed from LICENSE)0
-rw-r--r--README.md112
-rw-r--r--benchmarks/CMakeLists.txt0
-rw-r--r--docs/CPP_STYLE.md6
-rw-r--r--docs/logo.jpegbin0 -> 46868 bytes
m---------doctest0
-rw-r--r--include/just_gtfs/just_gtfs.h1923
-rw-r--r--tests/CMakeLists.txt10
-rw-r--r--tests/data/sample_feed/agency.txt2
-rw-r--r--tests/data/sample_feed/calendar.txt3
-rw-r--r--tests/data/sample_feed/calendar_dates.txt2
-rw-r--r--tests/data/sample_feed/fare_attributes.txt3
-rw-r--r--tests/data/sample_feed/fare_rules.txt5
-rw-r--r--tests/data/sample_feed/frequencies.txt12
-rw-r--r--tests/data/sample_feed/routes.txt6
-rw-r--r--tests/data/sample_feed/shapes.txt9
-rw-r--r--tests/data/sample_feed/stop_times.txt29
-rw-r--r--tests/data/sample_feed/stops.txt10
-rw-r--r--tests/data/sample_feed/trips.txt12
-rw-r--r--tests/unit_tests.cpp345
25 files changed, 2576 insertions, 2 deletions
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
index fa468d5..fa468d5 100644
--- a/LICENSE
+++ b/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
+
+[![GTFS parser for C++](https://github.com/mapsme/just_gtfs/blob/add-the-most-important-readers/docs/logo.jpeg)](https://github.com/mapsme/just_gtfs)
+
+[![C++](https://img.shields.io/badge/c%2B%2B-17-informational.svg)](https://shields.io/)
+[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/)
+![](https://github.com/mapsme/just_gtfs/workflows/C%2FC%2B%2B%20CI/badge.svg)
+[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](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
--- /dev/null
+++ b/benchmarks/CMakeLists.txt
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 0000000..22ad621
--- /dev/null
+++ b/docs/logo.jpeg
Binary files differ
diff --git a/doctest b/doctest
new file mode 160000
+Subproject 932a2ca50666138256dae56fbb16db3b1cae133
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();