diff --git a/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/model/RiderCategory.java b/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/model/RiderCategory.java index 36b4ab77..b285ccab 100644 --- a/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/model/RiderCategory.java +++ b/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/model/RiderCategory.java @@ -17,7 +17,7 @@ import org.onebusaway.csv_entities.schema.annotations.CsvField; import org.onebusaway.csv_entities.schema.annotations.CsvFields; -import org.onebusaway.gtfs.serialization.mappings.AgencyIdFieldMappingFactory; +import org.onebusaway.gtfs.annotations.Experimental; import org.onebusaway.gtfs.serialization.mappings.DefaultAgencyIdFieldMappingFactory; /** @@ -28,14 +28,30 @@ public final class RiderCategory extends IdentityBean { public static final int MISSING_VALUE = -999; + @Experimental(proposedBy = "https://github.com/google/transit/pull/511") @CsvField(name = "rider_category_id", mapping = DefaultAgencyIdFieldMappingFactory.class) private AgencyAndId id; - @CsvField(name = "rider_category_name", optional = true) + + @Experimental(proposedBy = "https://github.com/google/transit/pull/511") + @CsvField(name = "rider_category_name", optional = false) private String name; + + /** + * 0 = not default category, 1 = default category + */ + @Experimental(proposedBy = "https://github.com/google/transit/pull/511") + @CsvField(optional = true, defaultValue = "0") + private int isDefaultFareCategory = 0; + + @Deprecated @CsvField(optional = true) private int minAge = MISSING_VALUE; + + @Deprecated @CsvField(optional = true) private int maxAge = MISSING_VALUE; + + @Experimental(proposedBy = "https://github.com/google/transit/pull/511") @CsvField(optional = true) private String eligibilityUrl; @@ -47,6 +63,14 @@ public void setName(String name) { this.name = name; } + public int getIsDefaultFareCategory() { + return isDefaultFareCategory; + } + + public void setIsDefaultFareCategory(int isDefaultFareCategory) { + this.isDefaultFareCategory = isDefaultFareCategory; + } + public int getMinAge() { return minAge; } diff --git a/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/model/RiderCategoryTest.java b/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/model/RiderCategoryTest.java new file mode 100644 index 00000000..4317a651 --- /dev/null +++ b/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/model/RiderCategoryTest.java @@ -0,0 +1,121 @@ +/** + * Copyright (C) 2025 Sound Transit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onebusaway.gtfs.model; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onebusaway.gtfs.serialization.GtfsWriter; +import org.onebusaway.gtfs.serialization.GtfsWriterTest; +import org.onebusaway.gtfs.services.MockGtfs; +import org.onebusaway.gtfs.services.GtfsRelationalDao; + +import static org.junit.jupiter.api.Assertions.*; + +public class RiderCategoryTest { + + private MockGtfs _gtfs; + + private File _tmpDirectory; + + @BeforeEach + public void before() throws IOException { + _gtfs = MockGtfs.create(); + + //make temp directory for gtfs writing output + _tmpDirectory = File.createTempFile("RiderCategoryTest-", "-tmp"); + if (_tmpDirectory.exists()) + GtfsWriterTest.deleteFileRecursively(_tmpDirectory); + _tmpDirectory.mkdirs(); + } + + @Test + public void testBasicNetworks() throws IOException { + _gtfs.putMinimal(); + _gtfs.putDefaultTrips(); + _gtfs.putDefaultStops(); + _gtfs.putLines("rider_categories.txt", + "rider_category_id,rider_category_name,is_default_fare_category,eligibility_url", + "cat1,Adult,1,https://www.example.com/adult-fares", + "cat2,Reduced,0,https://www.example.com/reduced-fares", + "cat3,Youth,0,https://www.example.com/youth-fares" + ); + + GtfsRelationalDao dao = _gtfs.read(); + assertEquals(3, dao.getAllRiderCategories().size()); + + GtfsWriter writer = new GtfsWriter(); + writer.setOutputLocation(_tmpDirectory); + writer.run(dao); + + Scanner scan = new Scanner(new File(_tmpDirectory + "/rider_categories.txt")); + Set expectedRiderCategoryNames = new HashSet(); + Set foundRiderCategoryNames = new HashSet(); + expectedRiderCategoryNames.add("Adult"); + expectedRiderCategoryNames.add("Reduced"); + expectedRiderCategoryNames.add("Youth"); + boolean onHeader = true; + while(scan.hasNext()){ + String line = scan.nextLine(); + if (onHeader) { + onHeader = false; + } else { + String[] lineParts = line.split(","); + + if (lineParts.length > 1) { + foundRiderCategoryNames.add(lineParts[1]); + } + } + } + scan.close(); + + assertEquals(expectedRiderCategoryNames, foundRiderCategoryNames, "Did not find rider category names in output"); + } + + @Test + public void testPutMinimal() throws IOException { + _gtfs.putMinimal(); + // Just make sure it parses without throwing an error. + _gtfs.read(); + } + + @AfterEach + public void teardown() { + deleteFileRecursively(_tmpDirectory); + } + + public static void deleteFileRecursively(File file) { + + if (!file.exists()) + return; + + if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files != null) { + for (File child : files) + deleteFileRecursively(child); + } + } + + file.delete(); + } + +} + diff --git a/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/FaresV2ReaderTest.java b/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/FaresV2ReaderTest.java index 04923bcc..95bbcb70 100644 --- a/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/FaresV2ReaderTest.java +++ b/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/FaresV2ReaderTest.java @@ -87,6 +87,7 @@ public void turlockFaresV2() throws CsvEntityIOException, IOException { assertEquals("Youth Age 18 and Under", riderCat.getName()); assertEquals(18, riderCat.getMaxAge()); assertEquals(RiderCategory.MISSING_VALUE, riderCat.getMinAge()); + assertEquals(0, riderCat.getIsDefaultFareCategory(), "isDefaultFareCategory not 0 when unspecified"); assertEquals("http://www.turlocktransit.com/fares.html", riderCat.getEligibilityUrl()); assertTrue(dao.hasFaresV1()); @@ -134,6 +135,26 @@ public void mdotMetroFaresV2() throws CsvEntityIOException, IOException { assertEquals("charmcard_senior", medium.getId().getId()); assertEquals("Senior CharmCard", medium.getName()); + List riderCats = new ArrayList<>(dao.getAllRiderCategories()); + assertEquals(5, riderCats.size()); + + RiderCategory riderCat = riderCats.stream().sorted(Comparator.comparing(RiderCategory::getId)).filter(c -> c.getId().getId().equals("reg")).findAny().get(); + assertEquals("reg", riderCat.getId().getId()); + assertEquals("Regular", riderCat.getName()); + assertEquals(1, riderCat.getIsDefaultFareCategory()); + assertEquals(RiderCategory.MISSING_VALUE, riderCat.getMaxAge()); + assertEquals(RiderCategory.MISSING_VALUE, riderCat.getMinAge()); + assertEquals("https://www.mta.maryland.gov/regular-fares", riderCat.getEligibilityUrl()); + + RiderCategory riderCat2 = riderCats.stream().sorted(Comparator.comparing(RiderCategory::getId)).filter(c -> c.getId().getId().equals("sen")).findAny().get(); + assertEquals("sen", riderCat2.getId().getId()); + assertEquals("Senior", riderCat2.getName()); + assertEquals(0, riderCat2.getIsDefaultFareCategory()); + assertEquals(RiderCategory.MISSING_VALUE, riderCat2.getMaxAge()); + assertEquals(65, riderCat2.getMinAge()); + assertEquals("https://www.mta.maryland.gov/senior-reduced-fare-program", riderCat2.getEligibilityUrl()); + + List stopAreaElements = new ArrayList<>(dao.getAllStopAreaElements()); assertEquals(0, stopAreaElements.size()); diff --git a/onebusaway-gtfs/src/test/resources/org/onebusaway/gtfs/mdot-metro-fares-v2/rider_categories.txt b/onebusaway-gtfs/src/test/resources/org/onebusaway/gtfs/mdot-metro-fares-v2/rider_categories.txt index 1abbc1ac..ca9654b4 100644 --- a/onebusaway-gtfs/src/test/resources/org/onebusaway/gtfs/mdot-metro-fares-v2/rider_categories.txt +++ b/onebusaway-gtfs/src/test/resources/org/onebusaway/gtfs/mdot-metro-fares-v2/rider_categories.txt @@ -1,5 +1,6 @@ -rider_category_id,rider_category_name,min_age,max_age,eligibility_url -sen,Senior,65,,https://www.mta.maryland.gov/senior-reduced-fare-program -dis,Disability,,,https://www.mta.maryland.gov/disabled-reduced-fare-program -stu,Student,,,https://www.mta.maryland.gov/student-fares -mob,Mobility,,,https://www.mta.maryland.gov/mobility +rider_category_id,rider_category_name,is_default_fare_category,min_age,max_age,eligibility_url +reg,Regular,1,,,https://www.mta.maryland.gov/regular-fares +sen,Senior,0,65,,https://www.mta.maryland.gov/senior-reduced-fare-program +dis,Disability,0,,,https://www.mta.maryland.gov/disabled-reduced-fare-program +stu,Student,0,,,https://www.mta.maryland.gov/student-fares +mob,Mobility,0,,,https://www.mta.maryland.gov/mobility