Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
description = "Read GTFS (public transit timetables) files"
name = "gtfs-structures"
version = "0.43.0"
version = "0.44.0"
authors = [
"Tristram Gräbener <tristramg@gmail.com>",
"Antoine Desbordes <antoine.desbordes@gmail.com>",
Expand Down
1 change: 1 addition & 0 deletions fixtures/fares_v2/agency.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
agency_name,agency_url,agency_timezone,agency_lang
5 changes: 5 additions & 0 deletions fixtures/fares_v2/areas.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
area_id,area_name
ZN1,Zone 1 - Vancouver
ZN2,Zone 2 - Burnaby, Richmond and North Vancouver
ZN3,Zone 3 - Surrey and Coquitlam
sea_island,Sea Island (Vancouver Airport YVR Airport, Sea Island Centre, Templeton)
15 changes: 15 additions & 0 deletions fixtures/fares_v2/fare_leg_rules.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
leg_group_id,network_id,fare_product_id,from_area_id,to_area_id,rule_priority
flat_fare_leg,translink_bus,bus_flat_fare,,,
sea_island_ZN1,skytrain_seabus,sea_island_2_zone_fare,sea_island,ZN1,1
sea_island_ZN2,skytrain_seabus,sea_island_1_zone_fare,sea_island,ZN2,1
sea_island_ZN3,skytrain_seabus,sea_island_2_zone_fare,sea_island,ZN3,1
sea_island_sea_island,skytrain_seabus,sea_island_sea_island_fare,sea_island,sea_island,2
ZN1_ZN1,skytrain_seabus,1_zone_fare,ZN1,
ZN2_ZN2,skytrain_seabus,1_zone_fare,ZN2,
ZN3_ZN3,skytrain_seabus,1_zone_fare,ZN3,
ZN1_ZN2,skytrain_seabus,2_zone_fare,ZN1,
ZN1_ZN2,skytrain_seabus,2_zone_fare,ZN2,
ZN2_ZN3,skytrain_seabus,2_zone_fare,ZN2,
ZN2_ZN3,skytrain_seabus,2_zone_fare,ZN3,
ZN1_ZN3,skytrain_seabus,3_zone_fare,ZN1,
ZN1_ZN3,skytrain_seabus,3_zone_fare,ZN3,
6 changes: 6 additions & 0 deletions fixtures/fares_v2/fare_media.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fare_media_id,fare_media_name,fare_media_type
cash,Cash,0
contactless,Contactless,3
compass_card,Compass Card,2
compass_ticket,Compass Ticket,1
wallet,Mobile Wallet,3
12 changes: 12 additions & 0 deletions fixtures/fares_v2/fare_products.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
fare_product_id,fare_product_name,amount,currency,fare_media_id
bus_flat_fare,Bus Flat Fare,3.20,CAD,contactless
bus_flat_fare,Bus Flat Fare,3.20,CAD,cash
bus_flat_fare,Bus Flat Fare,2.60,CAD,compass_card
bus_flat_fare_daily,Daily pass,11.50,CAD,compass_card
bus_flat_fare_daily,Daily pass,11.50,CAD,compass_ticket
1_zone_fare,1-Zone Fare,3.20,CAD,contactless
2_zone_fare,2-Zone Fare,4.65,CAD,contactless
3_zone_fare,3-Zone Fare,6.35,CAD,contactless
sea_island_1_zone_fare,Sea Island travel + 1-zone Fare,8.20,CAD,contactless
sea_island_2_zone_fare,Sea Island travel + 2-zone fare,9.65,CAD,contactless
sea_island_sea_island_fare,Free fare inside Sea Island,0,CAD,contactless
3 changes: 3 additions & 0 deletions fixtures/fares_v2/networks.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
network_id,network_name
translink_bus,Translink Buses
skytrain_seabus,SkyTrain and SeaBus
3 changes: 3 additions & 0 deletions fixtures/fares_v2/rider_categories.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
rider_category_id,rider_category_name,is_default_fare_category,eligibility_url
adult,Adult,1,
concession,Concession,,https://www.translink.ca/transit-fares/pricing-and-fare-zones#fare-pricing
5 changes: 5 additions & 0 deletions fixtures/fares_v2/route_networks.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
route_id,network_id
10232,translink_bus
11201,translink_bus
13686,skytrain_seabus
30052,skytrain_seabus
1 change: 1 addition & 0 deletions fixtures/fares_v2/routes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
agency_id,route_id,route_type
9 changes: 9 additions & 0 deletions fixtures/fares_v2/stop_areas.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
stop_id,area_id
8039,ZN1
8066,ZN2
99901,ZN2
99902,ZN2
99903,ZN2
99901,sea_island
99902,sea_island
99903,sea_island
1 change: 1 addition & 0 deletions fixtures/fares_v2/stop_times.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
trip_id,arrival_time,departure_time,stop_id,stop_sequence,stop_time_desc,pickup_type,drop_off_type,timepoint
4 changes: 4 additions & 0 deletions fixtures/fares_v2/stops.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
stop_id,stop_name
8039,Waterfront Station @ Platform 2
8066,Edmonds Station @ Platform 1
99901,YVR-Airport Station @ Canada Line
1 change: 1 addition & 0 deletions fixtures/fares_v2/trips.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
route_id,service_id,trip_id,trip_headsign,trip_short_name,direction_id,block_id,wheelchair_accessible,bikes_allowed,trip_desc,shape_id
47 changes: 47 additions & 0 deletions src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,3 +610,50 @@ pub enum PathwayDirectionType {
#[serde(rename = "1")]
Bidirectional,
}

/// Defines the type of a [FareMedia]
#[derive(Debug, Deserialize, Serialize, Copy, Clone, PartialEq, Eq)]
pub enum FareMediaType {
/// Used when there is no fare media involved in purchasing or validating a fare product
#[serde(rename = "0")]
None,
/// Physical paper ticket
#[serde(rename = "1")]
PhysicalPaperTicket,
/// Physical transit card
#[serde(rename = "2")]
PhysicalTransitCard,
/// cEMV (contactless Europay, Mastercard and Visa)
#[serde(rename = "3")]
CEmv,
/// Mobile app
#[serde(rename = "4")]
MobileApp,
}

/// Specifies if an entry in rider_categories.txt should be considered the default category
#[derive(Debug, Serialize, Derivative, Copy, Clone, PartialEq, Eq, Hash)]
pub enum DefaultFareCategory {
/// Category is not considered the default.
NotDefault = 0,
/// Category is considered the default one.
Default = 1,
}

impl<'de> Deserialize<'de> for DefaultFareCategory {
fn deserialize<D>(deserializer: D) -> Result<DefaultFareCategory, D::Error>
where
D: Deserializer<'de>,
{
let s = <&str>::deserialize(deserializer)?;
Ok(match s {
"" | "0" => DefaultFareCategory::NotDefault,
"1" => DefaultFareCategory::Default,
&_ => {
return Err(serde::de::Error::custom(format!(
"Invalid value `{s}`, expected 0 or 1"
)))
}
})
}
}
16 changes: 15 additions & 1 deletion src/gtfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ pub struct Gtfs {
pub fare_attributes: HashMap<String, FareAttribute>,
/// All fare rules by `fare_id`
pub fare_rules: HashMap<String, Vec<FareRule>>,
/// All fare products by `fare_product_id`
pub fare_products: HashMap<String, Vec<FareProduct>>,
/// All fare media by `fare_media_id`
pub fare_media: HashMap<String, FareMedia>,
/// All rider categories by `rider_category_id`
pub rider_categories: HashMap<String, RiderCategory>,
/// All feed information. There is no identifier
pub feed_info: Vec<FeedInfo>,
}
Expand All @@ -63,7 +69,12 @@ impl TryFrom<RawGtfs> for Gtfs {

let mut fare_rules = HashMap::<String, Vec<FareRule>>::new();
for f in raw.fare_rules.unwrap_or_else(|| Ok(Vec::new()))? {
(*fare_rules.entry(f.fare_id.clone()).or_default()).push(f);
fare_rules.entry(f.fare_id.clone()).or_default().push(f);
}

let mut fare_products = HashMap::<String, Vec<FareProduct>>::new();
for f in raw.fare_products.unwrap_or_else(|| Ok(Vec::new()))? {
fare_products.entry(f.id.clone()).or_default().push(f);
}

Ok(Gtfs {
Expand All @@ -74,6 +85,9 @@ impl TryFrom<RawGtfs> for Gtfs {
shapes: to_shape_map(raw.shapes.unwrap_or_else(|| Ok(Vec::new()))?),
fare_attributes: to_map(raw.fare_attributes.unwrap_or_else(|| Ok(Vec::new()))?),
fare_rules,
fare_products,
fare_media: to_map(raw.fare_media.unwrap_or_else(|| Ok(Vec::new()))?),
rider_categories: to_map(raw.rider_categories.unwrap_or_else(|| Ok(Vec::new()))?),
feed_info: raw.feed_info.unwrap_or_else(|| Ok(Vec::new()))?,
calendar: to_map(raw.calendar.unwrap_or_else(|| Ok(Vec::new()))?),
calendar_dates: to_calendar_dates(
Expand Down
17 changes: 17 additions & 0 deletions src/gtfs_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ impl RawGtfsReader {
shapes: self.read_objs_from_optional_path(p, "shapes.txt"),
fare_attributes: self.read_objs_from_optional_path(p, "fare_attributes.txt"),
fare_rules: self.read_objs_from_optional_path(p, "fare_rules.txt"),
fare_products: self.read_objs_from_optional_path(p, "fare_products.txt"),
fare_media: self.read_objs_from_optional_path(p, "fare_media.txt"),
rider_categories: self.read_objs_from_optional_path(p, "rider_categories.txt"),
frequencies: self.read_objs_from_optional_path(p, "frequencies.txt"),
transfers: self.read_objs_from_optional_path(p, "transfers.txt"),
pathways: self.read_objs_from_optional_path(p, "pathways.txt"),
Expand Down Expand Up @@ -265,6 +268,9 @@ impl RawGtfsReader {
"trips.txt",
"fare_attributes.txt",
"fare_rules.txt",
"fare_products.txt",
"fare_media.txt",
"rider_categories.txt",
"frequencies.txt",
"transfers.txt",
"pathways.txt",
Expand Down Expand Up @@ -301,6 +307,17 @@ impl RawGtfsReader {
"fare_attributes.txt",
),
fare_rules: self.read_optional_file(&file_mapping, &mut archive, "fare_rules.txt"),
fare_products: self.read_optional_file(
&file_mapping,
&mut archive,
"fare_products.txt",
),
fare_media: self.read_optional_file(&file_mapping, &mut archive, "fare_media.txt"),
rider_categories: self.read_optional_file(
&file_mapping,
&mut archive,
"rider_categories.txt",
),
frequencies: self.read_optional_file(&file_mapping, &mut archive, "frequencies.txt"),
transfers: self.read_optional_file(&file_mapping, &mut archive, "transfers.txt"),
pathways: self.read_optional_file(&file_mapping, &mut archive, "pathways.txt"),
Expand Down
90 changes: 90 additions & 0 deletions src/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,96 @@ impl Type for FareAttribute {
}
}

/// Used to describe the range of fares available for purchase by riders or taken into account
/// when computing the total fare for journeys with multiple legs, such as transfer costs.
/// https://gtfs.org/documentation/schedule/reference/#fare_productstxt
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct FareProduct {
/// Identifies a fare product or set of fare products.
#[serde(rename = "fare_product_id")]
pub id: String,
/// The name of the fare product as displayed to riders.
#[serde(rename = "fare_product_name")]
pub name: Option<String>,
/// Identifies a rider category eligible for the fare product.
pub rider_category_id: Option<String>,
/// Identifies a fare media that can be employed to use the fare product during the trip.
pub fare_media_id: Option<String>,
/// The cost of the fare product. May be negative to represent transfer discounts. May be zero to represent a fare product that is free.
pub amount: String,
/// The currency of the cost of the fare product.
pub currency: String,
}

impl Id for FareProduct {
fn id(&self) -> &str {
&self.id
}
}

impl Type for FareProduct {
fn object_type(&self) -> ObjectType {
ObjectType::Fare
}
}

/// To describe the different fare media that can be employed to use fare products.
/// Fare media are physical or virtual holders used for the representation and/or validation of a fare product.
/// https://gtfs.org/documentation/schedule/reference/#fare_mediatxt
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct FareMedia {
/// Identifies a fare media.
#[serde(rename = "fare_media_id")]
pub id: String,
/// The name of the fare media.
#[serde(rename = "fare_media_name")]
pub name: Option<String>,
/// The type of fare media
#[serde(rename = "fare_media_type")]
pub media_type: FareMediaType,
}

impl Id for FareMedia {
fn id(&self) -> &str {
&self.id
}
}

impl Type for FareMedia {
fn object_type(&self) -> ObjectType {
ObjectType::Fare
}
}

/// Defines categories of riders (e.g. elderly, student).
/// https://gtfs.org/documentation/schedule/reference/#rider_categoriestxt
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct RiderCategory {
/// Identifies a rider category.
#[serde(rename = "rider_category_id")]
pub id: String,
/// Rider category name as displayed to the rider.
#[serde(rename = "rider_category_name")]
pub name: String,
/// is_default_fare_category
pub is_default_fare_category: DefaultFareCategory,
/// URL of a web page, usually from the operating agency, that provides
/// detailed information about a specific rider category and/or describes its eligibility criteria.
pub eligibility_url: Option<String>,
}

impl Id for RiderCategory {
fn id(&self) -> &str {
&self.id
}
}

impl Type for RiderCategory {
fn object_type(&self) -> ObjectType {
ObjectType::Fare
}
}

/// Defines one possible fare. See <https://gtfs.org/schedule/reference/#fare_rulestxt>
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct FareRule {
Expand Down
8 changes: 7 additions & 1 deletion src/raw_gtfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ pub struct RawGtfs {
pub agencies: Result<Vec<Agency>, Error>,
/// All shapes points, None if the file was absent as it is not mandatory
pub shapes: Option<Result<Vec<Shape>, Error>>,
/// All FareAttribates, None if the file was absent as it is not mandatory
/// All FareAttributes, None if the file was absent as it is not mandatory
pub fare_attributes: Option<Result<Vec<FareAttribute>, Error>>,
/// All FareRules, None if the file was absent as it is not mandatory
pub fare_rules: Option<Result<Vec<FareRule>, Error>>,
/// All FareProducts, None if the file was absent as it is not mandatory
pub fare_products: Option<Result<Vec<FareProduct>, Error>>,
/// All FareMedias, None if the file was absent as it is not mandatory
pub fare_media: Option<Result<Vec<FareMedia>, Error>>,
/// All RiderCategories, None if the file was absent as it is not mandatory
pub rider_categories: Option<Result<Vec<RiderCategory>, Error>>,
/// All Frequencies, None if the file was absent as it is not mandatory
pub frequencies: Option<Result<Vec<RawFrequency>, Error>>,
/// All Transfers, None if the file was absent as it is not mandatory
Expand Down
37 changes: 37 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,3 +524,40 @@ fn fare_v1() {
);
assert_eq!(gtfs.fare_rules, expected_rules);
}

#[test]
fn fares_v2() {
let gtfs = Gtfs::from_path("fixtures/fares_v2").expect("impossible to read gtfs");

let expected = vec![FareProduct {
id: "1_zone_fare".to_string(),
name: Some("1-Zone Fare".to_string()),
rider_category_id: None,
fare_media_id: Some("contactless".to_string()),
amount: "3.20".to_string(),
currency: "CAD".to_string(),
}];

assert_eq!(gtfs.fare_products.len(), 8);
assert_eq!(gtfs.fare_products["1_zone_fare"], expected);

let expected = FareMedia {
id: "contactless".to_string(),
name: Some("Contactless".to_string()),
media_type: FareMediaType::CEmv,
};
assert_eq!(gtfs.fare_media.len(), 5);
assert_eq!(gtfs.fare_media["contactless"], expected);

let expected = RiderCategory {
id: "concession".to_string(),
name: "Concession".to_string(),
is_default_fare_category: DefaultFareCategory::NotDefault,
eligibility_url: Some(
"https://www.translink.ca/transit-fares/pricing-and-fare-zones#fare-pricing"
.to_string(),
),
};
assert_eq!(gtfs.rider_categories.len(), 2);
assert_eq!(gtfs.rider_categories["concession"], expected);
}