diff --git a/include/just_gtfs/just_gtfs.h b/include/just_gtfs/just_gtfs.h index d4f792d..97446e2 100644 --- a/include/just_gtfs/just_gtfs.h +++ b/include/just_gtfs/just_gtfs.h @@ -455,6 +455,7 @@ class Time inline Time() = default; inline explicit Time(const std::string & raw_time_str); inline Time(uint16_t hours, uint16_t minutes, uint16_t seconds); + inline Time(size_t seconds); inline bool is_provided() const; inline size_t get_total_seconds() const; inline std::tuple get_hh_mm_ss() const; @@ -543,6 +544,16 @@ inline Time::Time(uint16_t hours, uint16_t minutes, uint16_t seconds) time_is_provided = true; } +inline Time::Time(size_t seconds) + : time_is_provided(true) + , total_seconds(seconds) + , hh(seconds / 3600) + , mm((seconds % 3600) / 60) + , ss(seconds % 3600) +{ + set_raw_time(); +} + inline bool Time::is_provided() const { return time_is_provided; } inline size_t Time::get_total_seconds() const { return total_seconds; } @@ -644,7 +655,7 @@ using CurrencyCode = std::string; using LanguageCode = std::string; // Helper enums for some GTFS fields --------------------------------------------------------------- -enum class StopLocationType +enum class StopLocationType : int8_t { StopOrPlatform = 0, Station = 1, @@ -654,7 +665,7 @@ enum class StopLocationType }; // The type of transportation used on a route. -enum class RouteType +enum class RouteType : int16_t { // GTFS route types Tram = 0, // Tram, Streetcar, Light rail @@ -752,20 +763,20 @@ enum class RouteType HorseDrawnCarriage = 1702 }; -enum class TripDirectionId +enum class TripDirectionId : bool { DefaultDirection = 0, // e.g. outbound OppositeDirection = 1 // e.g. inbound }; -enum class TripAccess +enum class TripAccess : int8_t { NoInfo = 0, Yes = 1, No = 2 }; -enum class StopTimeBoarding +enum class StopTimeBoarding : int8_t { RegularlyScheduled = 0, No = 1, // Not available @@ -773,31 +784,31 @@ enum class StopTimeBoarding CoordinateWithDriver = 3 // Must coordinate with driver to arrange }; -enum class StopTimePoint +enum class StopTimePoint : bool { Approximate = 0, Exact = 1 }; -enum class CalendarAvailability +enum class CalendarAvailability : bool { NotAvailable = 0, Available = 1 }; -enum class CalendarDateException +enum class CalendarDateException : int8_t { Added = 1, // Service has been added for the specified date Removed = 2 }; -enum class FarePayment +enum class FarePayment : bool { OnBoard = 0, BeforeBoarding = 1 // Fare must be paid before boarding }; -enum class FareTransfers +enum class FareTransfers : int8_t { No = 0, // No transfers permitted on this fare Once = 1, @@ -805,13 +816,13 @@ enum class FareTransfers Unlimited = 3 }; -enum class FrequencyTripService +enum class FrequencyTripService : bool { FrequencyBased = 0, // Frequency-based trips ScheduleBased = 1 // Schedule-based trips with the exact same headway throughout the day }; -enum class TransferType +enum class TransferType : int8_t { Recommended = 0, Timed = 1, @@ -819,7 +830,7 @@ enum class TransferType NotPossible = 3 }; -enum class PathwayMode +enum class PathwayMode : int8_t { Walkway = 1, Stairs = 2, @@ -830,13 +841,13 @@ enum class PathwayMode ExitGate = 7 }; -enum class PathwayDirection +enum class PathwayDirection : bool { Unidirectional = 0, Bidirectional = 1 }; -enum class AttributionRole +enum class AttributionRole : bool { No = 0, // Organization doesn’t have this role Yes = 1 // Organization does have this role @@ -888,7 +899,7 @@ struct Stop Text stop_code; Text stop_desc; Text stop_url; - StopLocationType location_type = StopLocationType::GenericNode; + StopLocationType location_type = StopLocationType::StopOrPlatform; Text stop_timezone; Text wheelchair_boarding; Id level_id; @@ -999,6 +1010,14 @@ struct FareAttributesItem size_t transfer_duration = 0; // Length of time in seconds before a transfer expires }; +inline bool operator==(const FareAttributesItem & lhs, const FareAttributesItem & rhs) +{ + return std::tie(lhs.fare_id, lhs.price, lhs.currency_type, lhs.payment_method, lhs.transfers, + lhs.agency_id, lhs.transfer_duration) == + std::tie(rhs.fare_id, rhs.price, rhs.currency_type, rhs.payment_method, rhs.transfers, + rhs.agency_id, rhs.transfer_duration); +} + // Optional dataset file struct FareRule { @@ -1340,6 +1359,7 @@ class Feed inline void write_translations(std::ofstream & out) const; inline void write_attributions(std::ofstream & out) const; +protected: std::string gtfs_directory; Agencies agencies; @@ -1840,7 +1860,7 @@ inline Result Feed::add_fare_attributes(const ParsedCsvRow & row) item.currency_type = row.at("currency_type"); set_field(item.payment_method, row, "payment_method", false); - set_field(item.transfers, row, "transfers", false); + set_field(item.transfers, row, "transfers"); // Conditionally optional: item.agency_id = get_value_or_default(row, "agency_id"); @@ -2778,9 +2798,12 @@ inline void Feed::write_fare_attributes(std::ofstream & out) const for (const auto & attribute : fare_attributes) { std::vector fields{ - wrap(attribute.fare_id), wrap(attribute.price), attribute.currency_type, - wrap(attribute.payment_method), wrap(attribute.transfers), wrap(attribute.agency_id), - wrap(attribute.transfer_duration)}; + wrap(attribute.fare_id), wrap(attribute.price), attribute.currency_type, + wrap(attribute.payment_method), + // Here we handle GTFS specification corner case: "The fact that this field can be left + // empty is an exception to the requirement that a Required field must not be empty.": + attribute.transfers == FareTransfers::Unlimited ? "" : wrap(attribute.transfers), + wrap(attribute.agency_id), wrap(attribute.transfer_duration)}; write_joined(out, std::move(fields)); } } diff --git a/tests/data/sample_feed/fare_attributes.txt b/tests/data/sample_feed/fare_attributes.txt index 3ee7a99..e237bff 100644 --- a/tests/data/sample_feed/fare_attributes.txt +++ b/tests/data/sample_feed/fare_attributes.txt @@ -1,3 +1,4 @@ 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 +a,5.25,USD,1,1, +x,20,USD,0,,60 diff --git a/tests/unit_tests.cpp b/tests/unit_tests.cpp index abf387c..a6ec8c0 100644 --- a/tests/unit_tests.cpp +++ b/tests/unit_tests.cpp @@ -310,7 +310,7 @@ TEST_CASE("Read GTFS feed") CHECK_EQ(feed.get_attributions().size(), 1); CHECK_EQ(feed.get_calendar().size(), 2); CHECK_EQ(feed.get_calendar_dates().size(), 1); - CHECK_EQ(feed.get_fare_attributes().size(), 2); + CHECK_EQ(feed.get_fare_attributes().size(), 3); CHECK_EQ(feed.get_fare_rules().size(), 4); CHECK(!feed.get_feed_info().feed_publisher_name.empty()); CHECK_EQ(feed.get_levels().size(), 3); @@ -395,7 +395,7 @@ TEST_CASE("Stops") 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_EQ(stops[0].location_type, StopLocationType::StopOrPlatform); CHECK(stops[0].zone_id.empty()); auto const & stop = feed.get_stop("FUR_CREEK_RES"); @@ -494,7 +494,7 @@ TEST_CASE("Fare attributes") REQUIRE_EQ(feed.read_fare_attributes(), ResultCode::OK); const auto & attributes = feed.get_fare_attributes(); - REQUIRE_EQ(attributes.size(), 2); + REQUIRE_EQ(attributes.size(), 3); CHECK_EQ(attributes[0].fare_id, "p"); CHECK_EQ(attributes[0].price, 1.25); CHECK_EQ(attributes[0].currency_type, "USD"); @@ -502,9 +502,28 @@ TEST_CASE("Fare attributes") CHECK_EQ(attributes[0].transfers, FareTransfers::No); CHECK_EQ(attributes[0].transfer_duration, 0); + CHECK_EQ(attributes[1].fare_id, "a"); + CHECK_EQ(attributes[1].price, 5.25); + CHECK_EQ(attributes[1].currency_type, "USD"); + CHECK_EQ(attributes[1].payment_method, FarePayment::BeforeBoarding); + CHECK_EQ(attributes[1].transfers, FareTransfers::Once); + CHECK_EQ(attributes[1].transfer_duration, 0); + + CHECK_EQ(attributes[2].fare_id, "x"); + CHECK_EQ(attributes[2].price, 20); + CHECK_EQ(attributes[2].currency_type, "USD"); + CHECK_EQ(attributes[2].payment_method, FarePayment::OnBoard); + CHECK_EQ(attributes[2].transfers, FareTransfers::Unlimited); + CHECK_EQ(attributes[2].transfer_duration, 60); + const auto & attributes_for_id = feed.get_fare_attributes("a"); REQUIRE_EQ(attributes_for_id.size(), 1); CHECK_EQ(attributes_for_id[0].price, 5.25); + + REQUIRE_EQ(feed.write_fare_attributes("data/output_feed"), ResultCode::OK); + Feed feed_copy("data/output_feed"); + REQUIRE_EQ(feed_copy.read_fare_attributes(), ResultCode::OK); + CHECK_EQ(attributes, feed_copy.get_fare_attributes()); } TEST_CASE("Fare rules")