/* * Licensed to GraphHopper GmbH under one or more contributor * license agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * GraphHopper GmbH licenses this file to you 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 com.graphhopper.gtfs.fare; import com.conveyal.gtfs.GTFSFeed; import com.conveyal.gtfs.model.Fare; import com.conveyal.gtfs.model.FareAttribute; import com.conveyal.gtfs.model.FareRule; import com.csvreader.CsvReader; import org.junit.Assert; import org.junit.experimental.theories.DataPoint; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; import java.io.IOException; import java.io.StringReader; import java.math.BigDecimal; import java.util.*; import java.util.stream.Collectors; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; import static org.junit.Assume.assumeThat; @RunWith(Theories.class) public class FareTest { // See https://code.google.com/archive/p/googletransitdatafeed/wikis/FareExamples.wiki public static @DataPoint Map<String, Fare> oneDollarUnlimitedTransfers = parseFares("only_fare,1.00,USD,0\n", ""); public static @DataPoint Map<String, Fare> oneDollarNoTransfers = parseFares("only_fare,1.00,USD,0,0\n", ""); public static @DataPoint Map<String, Fare> oneDollarTimeLimitedTransfers = parseFares("only_fare,1.00,USD,0,,5400\n", ""); public static @DataPoint Map<String, Fare> regularAndExpress = parseFares("local_fare,1.75,USD,0,0\n"+"express_fare,5.00,USD,0,0\n", "local_fare,Route_1\nexpress_fare,Route_2\nexpress_fare,Route3\n"); public static @DataPoint Map<String, Fare> withTransfersOrWithout = parseFares("simple_fare,1.75,USD,0,0\n"+"plustransfer_fare,2.00,USD,0,,5400", ""); public static @DataPoint Map<String, Fare> stationPairs = parseFares("!S1_to_S2,1.75,USD,0\n!S1_to_S3,3.25,USD,0\n!S1_to_S4,4.55,USD,0\n!S4_to_S1,5.65,USD,0\n", "!S1_to_S2,,S1,S2\n!S1_to_S3,,S1,S3\n!S1_to_S4,,S1,S4\n!S4_to_S1,,S4,S1\n"); public static @DataPoint Map<String, Fare> zones = parseFares("F1,4.15,USD,0\nF2,2.20,USD,0\nF3,2.20,USD,0\nF4,2.95,USD,0\nF5,1.25,USD,0\nF6,1.95,USD,0\nF7,1.95,USD,0\n", "F1,,,,1\nF1,,,,2\nF1,,,,3\nF2,,,,1\nF2,,,,2\nF3,,,,1\nF3,,,,3\nF4,,,,2\nF4,,,,3\nF5,,,,1\nF6,,,,2\nF7,,,,3\n"); public static @DataPoint Trip tripWithOneSegment; public static @DataPoint Trip tripWithTwoSegments; public static @DataPoint Trip shortTripWithTwoSegments; static { tripWithOneSegment = new Trip(); tripWithOneSegment.segments.add(new Trip.Segment("Route_1", 0, "S1", "S2", new HashSet<>(Arrays.asList("1","2","3")))); tripWithTwoSegments = new Trip(); tripWithTwoSegments.segments.add(new Trip.Segment("Route_1", 0, "S1", "S4", new HashSet<>(Arrays.asList("1")))); tripWithTwoSegments.segments.add(new Trip.Segment("Route_2", 6000, "S4", "S1", new HashSet<>(Arrays.asList("1")))); shortTripWithTwoSegments = new Trip(); shortTripWithTwoSegments.segments.add(new Trip.Segment("Route_1",0, "S1", "S4", new HashSet<>(Arrays.asList("2", "3")))); shortTripWithTwoSegments.segments.add(new Trip.Segment("Route_2",5000, "S4", "S1", new HashSet<>(Arrays.asList("2", "3")))); } @Theory public void irrelevantAlternatives(Map<String, Fare> fares, Trip trip) { assumeThat("There are at least two fares.", fares.entrySet().size(), is(greaterThanOrEqualTo(2))); // If we only use one fare, say, the most expensive one... Fare mostExpensiveFare = fares.values().stream().max(Comparator.comparingDouble(f -> f.fare_attribute.price)).get(); HashMap<String, Fare> singleFare = new HashMap<>(); singleFare.put(mostExpensiveFare.fare_id, mostExpensiveFare); // ..and that still works for our trip.. assumeThat("There is at least one fare for each segment.", trip.segments.stream().map(segment -> Fares.possibleFares(singleFare, segment)).collect(Collectors.toList()), everyItem(is(not(empty())))); double priceWithOneOption = Fares.cheapestFare(singleFare, trip).get().getAmount().doubleValue(); double priceWithAllOptions = Fares.cheapestFare(fares, trip).get().getAmount().doubleValue(); assertThat("...it shouldn't get more expensive when we put the cheaper options back.", priceWithAllOptions, lessThanOrEqualTo(priceWithOneOption)); } @Theory public void everySegmentHasAFare(Map<String, Fare> fares, Trip trip) { assumeThat("There are fares.", fares.entrySet(), not(empty())); assertThat("There is at least one fare for each segment.", trip.segments.stream().map(segment -> Fares.possibleFares(fares, segment)).collect(Collectors.toList()), everyItem(is(not(empty())))); } @Theory public void withNoTransfersAndNoAlternativesBuyOneTicketForEachSegment(Map<String, Fare> fares, Trip trip) throws IOException { fares.values().forEach(fare -> { assumeThat("No Transfers allowed.", fare.fare_attribute.transfers, equalTo(0)); }); trip.segments.stream() .map(segment -> Fares.possibleFares(fares, segment)) .forEach(candidateFares -> assertThat("Only one fare candidate per segment.", candidateFares.size(), equalTo(1))); assertThat("Total fare is the sum of all individual fares.", Fares.cheapestFare(fares, trip).get().getAmount().doubleValue(), equalTo(trip.segments.stream().flatMap(segment -> Fares.possibleFares(fares, segment).stream()).mapToDouble(fare -> fare.fare_attribute.price).sum())); } @Theory public void canGoAllTheWayOnOneTicket(Map<String, Fare> fares, Trip trip) throws IOException { assumeThat("Only one fare.", fares.size(), equalTo(1)); Fare onlyFare = fares.values().iterator().next(); assumeThat("Fare allows the number of transfers we need for our trip.", onlyFare.fare_attribute.transfers, greaterThanOrEqualTo(trip.segments.size())); assumeThat("Fare allows the time we need for our trip.", (long) onlyFare.fare_attribute.transfer_duration, greaterThanOrEqualTo(trip.segments.get(trip.segments.size()-1).getStartTime() - trip.segments.get(0).getStartTime())); Amount amount = Fares.cheapestFare(fares, trip).get(); Assert.assertEquals(BigDecimal.valueOf(onlyFare.fare_attribute.price), amount.getAmount()); } @Theory public void buyMoreThanOneTicketIfTripIsLongerThanAllowedOnOne(Map<String, Fare> fares, Trip trip) throws IOException { assumeThat("Only one fare.", fares.size(), equalTo(1)); Fare onlyFare = fares.values().iterator().next(); assumeThat("We have a transfer", trip.segments.size(), greaterThan(1)); assumeThat("Fare allows the number of transfers we need for our trip.", onlyFare.fare_attribute.transfers, greaterThanOrEqualTo(trip.segments.size())); assumeThat("Fare does not allow the time we need for our trip.", (long) onlyFare.fare_attribute.transfer_duration, lessThan(trip.segments.get(trip.segments.size()-1).getStartTime() - trip.segments.get(0).getStartTime())); Amount amount = Fares.cheapestFare(fares, trip).get(); assertThat(amount.getAmount().doubleValue(), greaterThan(onlyFare.fare_attribute.price)); } private static Map<String, Fare> parseFares(String fareAttributes, String fareRules) { GTFSFeed feed = new GTFSFeed(); HashMap<String, Fare> fares = new HashMap<>(); new FixedFareAttributeLoader(feed, fares) { void load(String input){ reader = new CsvReader(new StringReader(input)); reader.setHeaders(new String[]{"fare_id","price","currency_type","payment_method","transfers","transfer_duration"}); try { while (reader.readRecord()) { loadOneRow(); } } catch (IOException e) { e.printStackTrace(); } } }.load(fareAttributes); new FareRule.Loader(feed, fares) { void load(String input){ reader = new CsvReader(new StringReader(input)); reader.setHeaders(new String[]{"fare_id","route_id","origin_id","destination_id","contains_id"}); try { while (reader.readRecord()) { loadOneRow(); } } catch (IOException e) { e.printStackTrace(); } } }.load(fareRules); return fares; } }