use crate::Duration;
use crate::Journey;
use crate::RaptorCache;
use crate::RaptorCachePool;
use crate::SecondOfDay;
use crate::Timetable;
use crate::manual::SimpleTimetable;
macro_rules! plan {
($tt:expr; $(($route:expr, $stop:expr)),* $(,)?) => {
vec![$(($tt.route_idx_of(&$route), $tt.stop_idx_of(&$stop))),*]
};
}
#[test]
fn reboarding_picks_correct_boarding_stop() {
use Route::*;
use Stop::*;
use Trip::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
S,
A,
B,
C,
D,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
R2,
R3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
R1T1,
R2T1,
R3Late,
R3Early,
}
let tt = SimpleTimetable::new()
.route(
R1,
&[S, A],
&[(
R1T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(100), SecondOfDay(100)),
],
)],
)
.route(
R2,
&[S, B],
&[(
R2T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(30), SecondOfDay(30)),
],
)],
)
.route(
R3,
&[A, B, C, D],
&[
(
R3Late,
&[
(SecondOfDay(105), SecondOfDay(105)),
(SecondOfDay(110), SecondOfDay(110)),
(SecondOfDay(120), SecondOfDay(120)),
(SecondOfDay(130), SecondOfDay(130)),
],
),
(
R3Early,
&[
(SecondOfDay(25), SecondOfDay(25)),
(SecondOfDay(30), SecondOfDay(30)),
(SecondOfDay(40), SecondOfDay(40)),
(SecondOfDay(50), SecondOfDay(50)),
],
),
],
);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&S), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&D), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert!(!journeys.is_empty(), "should find at least one journey");
let best = journeys.iter().min_by_key(|j| j.arrival()).unwrap();
assert_eq!(best.arrival(), SecondOfDay(50));
assert_eq!(best.plan, plan!(tt; (R2, B), (R3, D)));
}
#[test]
fn no_journey_disconnected_graph() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
C,
D,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
T2,
}
let tt = SimpleTimetable::new()
.route(
Route::R1,
&[Stop::A, Stop::B],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
)
.route(
Route::R2,
&[Stop::C, Stop::D],
&[(
Trip::T2,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::D), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert!(
journeys.is_empty(),
"disconnected graph should yield no journeys"
);
}
#[test]
fn no_journey_missed_connection() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
C,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
T2,
}
let tt = SimpleTimetable::new()
.route(
Route::R1,
&[Stop::A, Stop::B],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(50), SecondOfDay(50)),
],
)],
)
.route(
Route::R2,
&[Stop::B, Stop::C],
&[(
Trip::T2,
&[
(SecondOfDay(0), SecondOfDay(30)),
(SecondOfDay(40), SecondOfDay(40)),
],
)],
);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::C), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert!(
journeys.is_empty(),
"missed connection should yield no journeys"
);
}
#[test]
fn no_journey_late_departure() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
}
let tt = SimpleTimetable::new().route(
Route::R1,
&[Stop::A, Stop::B],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(10)),
(SecondOfDay(20), SecondOfDay(20)),
],
)],
);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::B), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(100))
.run();
assert!(
journeys.is_empty(),
"late departure should yield no journeys"
);
}
#[test]
fn no_journey_transfers_zero() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
}
let tt = SimpleTimetable::new().route(
Route::R1,
&[Stop::A, Stop::B],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::B), Duration::ZERO)])
.max_transfers(0)
.depart_at(SecondOfDay(0))
.run();
assert!(journeys.is_empty(), "transfers=0 should yield no journeys");
}
#[test]
fn source_equals_target() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
}
let tt = SimpleTimetable::new().route(
Route::R1,
&[Stop::A, Stop::B],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert!(
journeys.is_empty(),
"source == target should yield no journeys"
);
}
#[test]
fn direct_journey_single_route() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
C,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
}
let tt = SimpleTimetable::new().route(
Route::R1,
&[Stop::A, Stop::B, Stop::C],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
(SecondOfDay(20), SecondOfDay(20)),
],
)],
);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::C), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(journeys.len(), 1);
assert_eq!(journeys[0].arrival(), SecondOfDay(20));
assert_eq!(journeys[0].plan, plan!(tt; (Route::R1, Stop::C)));
}
#[test]
fn direct_journey_picks_fastest_route() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
T2,
}
let tt = SimpleTimetable::new()
.route(
Route::R1,
&[Stop::A, Stop::B],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(100), SecondOfDay(100)),
],
)],
)
.route(
Route::R2,
&[Stop::A, Stop::B],
&[(
Trip::T2,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(50), SecondOfDay(50)),
],
)],
);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::B), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
let best = journeys.iter().min_by_key(|j| j.arrival()).unwrap();
assert_eq!(best.arrival(), SecondOfDay(50));
}
#[test]
fn exact_time_connection() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
C,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
T2,
}
let tt = SimpleTimetable::new()
.route(
Route::R1,
&[Stop::A, Stop::B],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(20), SecondOfDay(20)),
],
)],
)
.route(
Route::R2,
&[Stop::B, Stop::C],
&[(
Trip::T2,
&[
(SecondOfDay(0), SecondOfDay(20)),
(SecondOfDay(30), SecondOfDay(30)),
],
)],
);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::C), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert!(!journeys.is_empty(), "exact-time connection should work");
let best = journeys.iter().min_by_key(|j| j.arrival()).unwrap();
assert_eq!(best.arrival(), SecondOfDay(30));
assert_eq!(
best.plan,
plan!(tt; (Route::R1, Stop::B), (Route::R2, Stop::C))
);
}
#[test]
fn multi_trip_picks_earliest_catchable() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
T2,
T3,
}
let tt = SimpleTimetable::new().route(
Route::R1,
&[Stop::A, Stop::B],
&[
(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(5)),
(SecondOfDay(15), SecondOfDay(15)),
],
),
(
Trip::T2,
&[
(SecondOfDay(0), SecondOfDay(15)),
(SecondOfDay(25), SecondOfDay(25)),
],
),
(
Trip::T3,
&[
(SecondOfDay(0), SecondOfDay(25)),
(SecondOfDay(35), SecondOfDay(35)),
],
),
],
);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::B), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(12))
.run();
assert_eq!(journeys.len(), 1);
assert_eq!(journeys[0].arrival(), SecondOfDay(25)); }
#[test]
fn two_transfer_journey() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
C,
D,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
R2,
R3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
T2,
T3,
}
let tt = SimpleTimetable::new()
.route(
Route::R1,
&[Stop::A, Stop::B],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
)
.route(
Route::R2,
&[Stop::B, Stop::C],
&[(
Trip::T2,
&[
(SecondOfDay(0), SecondOfDay(10)),
(SecondOfDay(20), SecondOfDay(20)),
],
)],
)
.route(
Route::R3,
&[Stop::C, Stop::D],
&[(
Trip::T3,
&[
(SecondOfDay(0), SecondOfDay(20)),
(SecondOfDay(30), SecondOfDay(30)),
],
)],
);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::D), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert!(!journeys.is_empty());
let best = journeys.iter().min_by_key(|j| j.arrival()).unwrap();
assert_eq!(best.arrival(), SecondOfDay(30));
assert_eq!(
best.plan,
plan!(tt;
(Route::R1, Stop::B),
(Route::R2, Stop::C),
(Route::R3, Stop::D),
)
);
}
#[test]
fn pareto_optimal_fewer_transfers_vs_faster() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
D,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
R2,
R3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
T2,
T3,
}
let tt = SimpleTimetable::new()
.route(
Route::R1,
&[Stop::A, Stop::D],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(200), SecondOfDay(200)),
],
)],
)
.route(
Route::R2,
&[Stop::A, Stop::B],
&[(
Trip::T2,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(40), SecondOfDay(40)),
],
)],
)
.route(
Route::R3,
&[Stop::B, Stop::D],
&[(
Trip::T3,
&[
(SecondOfDay(0), SecondOfDay(40)),
(SecondOfDay(100), SecondOfDay(100)),
],
)],
);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::D), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(journeys.len(), 2, "should have 2 pareto-optimal journeys");
let mut sorted = journeys.clone();
sorted.sort_by_key(|j| j.arrival());
assert_eq!(sorted[0].arrival(), SecondOfDay(100));
assert_eq!(sorted[0].plan.len(), 2);
assert_eq!(sorted[1].arrival(), SecondOfDay(200));
assert_eq!(sorted[1].plan.len(), 1);
}
#[test]
fn footpath_enables_connection() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
C,
D,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
T2,
}
let tt = SimpleTimetable::new()
.route(
Route::R1,
&[Stop::A, Stop::B],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
)
.route(
Route::R2,
&[Stop::C, Stop::D],
&[(
Trip::T2,
&[
(SecondOfDay(0), SecondOfDay(20)),
(SecondOfDay(30), SecondOfDay(30)),
],
)],
)
.footpath(Stop::B, Stop::C)
.transfer_time(Stop::B, Stop::C, Duration(5));
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::D), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(journeys.len(), 1, "expected one journey, got {journeys:?}");
assert_eq!(journeys[0].arrival(), SecondOfDay(30));
assert_eq!(
journeys[0].plan,
plan!(tt; (Route::R1, Stop::B), (Route::R2, Stop::D))
);
}
#[test]
fn footpath_transfer_time_causes_miss() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
C,
D,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
T2,
}
let tt = SimpleTimetable::new()
.route(
Route::R1,
&[Stop::A, Stop::B],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(50), SecondOfDay(50)),
],
)],
)
.route(
Route::R2,
&[Stop::C, Stop::D],
&[(
Trip::T2,
&[
(SecondOfDay(0), SecondOfDay(52)),
(SecondOfDay(60), SecondOfDay(60)),
],
)],
)
.footpath(Stop::B, Stop::C)
.transfer_time(Stop::B, Stop::C, Duration(5));
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::D), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert!(
journeys.is_empty(),
"footpath transfer time should cause miss"
);
}
#[test]
fn early_termination_no_improvement() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
}
let tt = SimpleTimetable::new().route(
Route::R1,
&[Stop::A, Stop::B],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
);
let j1 = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::B), Duration::ZERO)])
.max_transfers(1)
.depart_at(SecondOfDay(0))
.run();
let j100 = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::B), Duration::ZERO)])
.max_transfers(100)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(j1.len(), j100.len());
assert_eq!(
j1.iter().min_by_key(|j| j.arrival()).unwrap().arrival(),
j100.iter().min_by_key(|j| j.arrival()).unwrap().arrival(),
);
}
#[test]
fn dominance_prunes_slower_arrival() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
T2,
}
let tt = SimpleTimetable::new()
.route(
Route::R1,
&[Stop::A, Stop::B],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(50), SecondOfDay(50)),
],
)],
)
.route(
Route::R2,
&[Stop::A, Stop::B],
&[(
Trip::T2,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(100), SecondOfDay(100)),
],
)],
);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&Stop::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&Stop::B), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(journeys.len(), 1, "dominated journey should be pruned");
assert_eq!(journeys[0].arrival(), SecondOfDay(50));
}
#[test]
fn raptor_with_cache_matches_fresh_run() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
C,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
T2,
}
let tt = SimpleTimetable::new()
.route(
Route::R1,
&[Stop::A, Stop::B],
&[(
Trip::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
)
.route(
Route::R2,
&[Stop::B, Stop::C],
&[(
Trip::T2,
&[
(SecondOfDay(15), SecondOfDay(15)),
(SecondOfDay(25), SecondOfDay(25)),
],
)],
);
let queries = [
(3, SecondOfDay(0), Stop::A, Stop::C),
(1, SecondOfDay(0), Stop::A, Stop::B),
(5, SecondOfDay(5), Stop::A, Stop::C),
(2, SecondOfDay(0), Stop::B, Stop::C),
];
let mut cache: RaptorCache = RaptorCache::for_timetable(&tt);
for &(transfers, depart, ps, pt) in &queries {
let ps_idx = tt.stop_idx_of(&ps);
let pt_idx = tt.stop_idx_of(&pt);
let baseline = tt
.query()
.from(&[(ps_idx, Duration::ZERO)])
.to(&[(pt_idx, Duration::ZERO)])
.max_transfers(transfers as u8)
.depart_at(depart)
.run();
let cached = tt
.query()
.from(&[(ps_idx, Duration::ZERO)])
.to(&[(pt_idx, Duration::ZERO)])
.max_transfers(transfers as u8)
.depart_at(depart)
.run_with_cache(&mut cache);
assert_eq!(
cached.len(),
baseline.len(),
"journey count differs at query {ps:?}->{pt:?}"
);
for (b, c) in baseline.iter().zip(cached.iter()) {
assert_eq!(b.arrival(), c.arrival());
assert_eq!(b.plan, c.plan);
}
}
}
#[test]
fn multi_source_picks_best_origin() {
use Route::*;
use Stop::*;
use Trip::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
C,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
RFast,
RSlow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
TFast,
TSlow,
}
let tt = SimpleTimetable::new()
.route(
RFast,
&[A, C],
&[(
TFast,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
)
.route(
RSlow,
&[B, C],
&[(
TSlow,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(30), SecondOfDay(30)),
],
)],
);
let a = tt.stop_idx_of(&A);
let b = tt.stop_idx_of(&B);
let c = tt.stop_idx_of(&C);
let journeys = tt
.query()
.from(&[(a, Duration::ZERO), (b, Duration::ZERO)])
.to(&[(c, Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(journeys.len(), 1, "one Pareto-optimal journey expected");
assert_eq!(journeys[0].arrival(), SecondOfDay(10));
assert_eq!(
journeys[0].origin, a,
"should have started at A (the faster route)"
);
assert_eq!(journeys[0].target, c);
assert_eq!(journeys[0].plan, plan!(tt; (RFast, C)));
}
#[test]
fn multi_source_walk_offset_changes_best_origin() {
use Route::*;
use Stop::*;
use Trip::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
C,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
RFast,
RSlow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
TFast,
TSlow,
}
let tt = SimpleTimetable::new()
.route(
RFast,
&[A, C],
&[(
TFast,
&[
(SecondOfDay(30), SecondOfDay(30)),
(SecondOfDay(40), SecondOfDay(40)),
],
)],
)
.route(
RSlow,
&[B, C],
&[(
TSlow,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(30), SecondOfDay(30)),
],
)],
);
let a = tt.stop_idx_of(&A);
let b = tt.stop_idx_of(&B);
let c = tt.stop_idx_of(&C);
let journeys = tt
.query()
.from(&[(a, Duration(30)), (b, Duration::ZERO)])
.to(&[(c, Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
let best = journeys.iter().min_by_key(|j| j.arrival()).unwrap();
assert_eq!(best.arrival(), SecondOfDay(30));
assert_eq!(
best.origin, b,
"should have started at B (closer + slow trip wins)"
);
}
#[test]
fn multi_target_walk_offset_picks_best_target() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
T1,
T2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
Tr1,
Tr2,
}
let tt = SimpleTimetable::new()
.route(
Route::R1,
&[Stop::A, Stop::T1],
&[(
Trip::Tr1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
)
.route(
Route::R2,
&[Stop::A, Stop::T2],
&[(
Trip::Tr2,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(25), SecondOfDay(25)),
],
)],
);
let a = tt.stop_idx_of(&Stop::A);
let t1 = tt.stop_idx_of(&Stop::T1);
let t2 = tt.stop_idx_of(&Stop::T2);
let journeys = tt
.query()
.from(&[(a, Duration::ZERO)])
.to(&[(t1, Duration(30)), (t2, Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
let best = journeys.iter().min_by_key(|j| j.arrival()).unwrap();
assert_eq!(best.arrival(), SecondOfDay(25));
assert_eq!(best.target, t2);
}
#[test]
fn query_builder_single_departure() {
use crate::labels::ArrivalAndWalk;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum S {
A,
B,
C,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum R {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Tr {
T1,
}
let tt = SimpleTimetable::new().route(
R::R1,
&[S::A, S::B, S::C],
&[(
Tr::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
(SecondOfDay(20), SecondOfDay(20)),
],
)],
);
let a = tt.stop_idx_of(&S::A);
let c = tt.stop_idx_of(&S::C);
let journeys = tt.query().from(a).to(c).depart_at(SecondOfDay(0)).run();
assert_eq!(journeys.len(), 1);
assert_eq!(journeys[0].arrival(), SecondOfDay(20));
let journeys = tt
.query()
.from(a)
.to(c)
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(journeys.len(), 1);
let mut cache = crate::RaptorCache::for_timetable(&tt);
let journeys = tt
.query()
.from(a)
.to(c)
.depart_at(SecondOfDay(0))
.run_with_cache(&mut cache);
assert_eq!(journeys.len(), 1);
let journeys: Vec<crate::Journey<ArrivalAndWalk>> = tt
.query_with_label::<ArrivalAndWalk>()
.from(a)
.to(c)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(journeys.len(), 1);
assert_eq!(journeys[0].label.walk_time, Duration::ZERO);
}
#[test]
fn query_builder_range_departure() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum S {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum R {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Tr {
T1,
T2,
T3,
}
let tt = SimpleTimetable::new().route(
R::R1,
&[S::A, S::B],
&[
(
Tr::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
),
(
Tr::T2,
&[
(SecondOfDay(10), SecondOfDay(10)),
(SecondOfDay(20), SecondOfDay(20)),
],
),
(
Tr::T3,
&[
(SecondOfDay(20), SecondOfDay(20)),
(SecondOfDay(30), SecondOfDay(30)),
],
),
],
);
let a = tt.stop_idx_of(&S::A);
let b = tt.stop_idx_of(&S::B);
let profile = tt
.query()
.from(a)
.to(b)
.depart_in_window([
SecondOfDay(0),
SecondOfDay(5),
SecondOfDay(10),
SecondOfDay(15),
SecondOfDay(20),
])
.run();
let mut points: Vec<(SecondOfDay, SecondOfDay)> = profile
.iter()
.map(|p| (p.depart, p.journey.arrival()))
.collect();
points.sort();
assert_eq!(
points,
vec![
(SecondOfDay(0), SecondOfDay(10)),
(SecondOfDay(10), SecondOfDay(20)),
(SecondOfDay(20), SecondOfDay(30))
]
);
}
#[test]
fn into_endpoints_accepts_natural_input_shapes() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum S {
A,
B,
C,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum R {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Tr {
T1,
}
let tt = SimpleTimetable::new().route(
R::R1,
&[S::A, S::B, S::C],
&[(
Tr::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
(SecondOfDay(20), SecondOfDay(20)),
],
)],
);
let a = tt.stop_idx_of(&S::A);
let b = tt.stop_idx_of(&S::B);
let c = tt.stop_idx_of(&S::C);
let j = tt
.query()
.from(a)
.to(c)
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(j.len(), 1);
assert_eq!(j[0].arrival(), SecondOfDay(20));
let j = tt
.query()
.from((a, Duration::ZERO))
.to(c)
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(j[0].arrival(), SecondOfDay(20));
let stops = [a, b];
let j = tt
.query()
.from(&stops[..])
.to(c)
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(j[0].arrival(), SecondOfDay(20));
let pairs = [(a, Duration::ZERO)];
let j = tt
.query()
.from(&pairs[..])
.to(&[(c, Duration::ZERO)][..])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(j[0].arrival(), SecondOfDay(20));
let owned = vec![(a, Duration::ZERO)];
let j = tt
.query()
.from(&owned)
.to(c)
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(j[0].arrival(), SecondOfDay(20));
let mut ep = crate::Endpoints::new();
ep.push(a, Duration::ZERO);
let j = tt
.query()
.from(ep)
.to(c)
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(j[0].arrival(), SecondOfDay(20));
}
#[test]
fn closed_path_dispatch_matches_dijkstra() {
use crate::{RouteIdx, SecondOfDay, StopIdx, TripIdx};
struct ClosedAssert<T: Timetable>(T);
impl<T: Timetable> Timetable for ClosedAssert<T> {
fn n_stops(&self) -> usize {
self.0.n_stops()
}
fn n_routes(&self) -> usize {
self.0.n_routes()
}
fn get_routes_serving_stop(&self, stop: StopIdx) -> &[(RouteIdx, u32)] {
self.0.get_routes_serving_stop(stop)
}
fn get_stops_after(&self, route: RouteIdx, pos: u32) -> &[StopIdx] {
self.0.get_stops_after(route, pos)
}
fn stop_at(&self, route: RouteIdx, pos: u32) -> StopIdx {
self.0.stop_at(route, pos)
}
fn get_earliest_trip(&self, route: RouteIdx, at: SecondOfDay, pos: u32) -> Option<TripIdx> {
self.0.get_earliest_trip(route, at, pos)
}
fn get_arrival_time(&self, trip: TripIdx, pos: u32) -> SecondOfDay {
self.0.get_arrival_time(trip, pos)
}
fn get_departure_time(&self, trip: TripIdx, pos: u32) -> SecondOfDay {
self.0.get_departure_time(trip, pos)
}
fn get_footpaths_from(&self, stop: StopIdx) -> &[StopIdx] {
self.0.get_footpaths_from(stop)
}
fn get_transfer_time(&self, from: StopIdx, to: StopIdx) -> Duration {
self.0.get_transfer_time(from, to)
}
fn footpaths_are_transitively_closed(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum S {
A,
B,
C,
D,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum R {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Tr {
T1,
T2,
}
let inner = SimpleTimetable::new()
.route(
R::R1,
&[S::A, S::B],
&[(
Tr::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
)
.route(
R::R2,
&[S::C, S::D],
&[(
Tr::T2,
&[
(SecondOfDay(0), SecondOfDay(15)),
(SecondOfDay(25), SecondOfDay(25)),
],
)],
)
.footpath(S::B, S::C)
.transfer_time(S::B, S::C, Duration(3));
let a = inner.stop_idx_of(&S::A);
let d = inner.stop_idx_of(&S::D);
let dijkstra = inner
.query()
.from(&[(a, Duration::ZERO)])
.to(&[(d, Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
let closed = ClosedAssert(inner)
.query()
.from(a)
.to(d)
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(dijkstra.len(), closed.len(), "journey count must match");
for (a, b) in dijkstra.iter().zip(closed.iter()) {
assert_eq!(a.arrival(), b.arrival(), "arrival must match");
assert_eq!(a.plan, b.plan, "plan must match");
}
}
#[test]
fn arrival_and_walk_label_tracks_accumulated_walk_time() {
use crate::Duration;
use crate::RaptorCache;
use crate::labels::ArrivalAndWalk;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum S {
A,
B,
C,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum R {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Tr {
T1,
T2,
}
let tt = SimpleTimetable::new()
.route(
R::R1,
&[S::A, S::B],
&[(
Tr::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
)
.route(
R::R2,
&[S::C],
&[(Tr::T2, &[(SecondOfDay(20), SecondOfDay(20))])],
)
.footpath(S::B, S::C)
.transfer_time(S::B, S::C, Duration(7));
let a = tt.stop_idx_of(&S::A);
let c = tt.stop_idx_of(&S::C);
let default_journeys = tt
.query()
.from(&[(a, Duration::ZERO)])
.to(&[(c, Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert!(!default_journeys.is_empty());
assert_eq!(default_journeys[0].arrival(), SecondOfDay(17));
let label_journeys: Vec<crate::Journey<ArrivalAndWalk>> = tt
.query_with_label::<ArrivalAndWalk>()
.from(a)
.to(c)
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(label_journeys.len(), default_journeys.len());
assert_eq!(label_journeys[0].arrival(), SecondOfDay(17));
assert_eq!(
label_journeys[0].label.walk_time,
Duration(7),
"walk time tracked"
);
let mut cache = RaptorCache::<ArrivalAndWalk>::for_timetable(&tt);
let cached = tt
.query_with_label::<ArrivalAndWalk>()
.from(a)
.to(c)
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run_with_cache(&mut cache);
assert_eq!(cached.len(), 1);
assert_eq!(cached[0].label.walk_time, Duration(7));
}
#[test]
fn raptor_range_returns_pareto_profile_across_departures() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum S {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum R {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Tr {
T1,
T2,
T3,
}
let tt = SimpleTimetable::new().route(
R::R1,
&[S::A, S::B],
&[
(
Tr::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
),
(
Tr::T2,
&[
(SecondOfDay(10), SecondOfDay(10)),
(SecondOfDay(20), SecondOfDay(20)),
],
),
(
Tr::T3,
&[
(SecondOfDay(20), SecondOfDay(20)),
(SecondOfDay(30), SecondOfDay(30)),
],
),
],
);
let a = tt.stop_idx_of(&S::A);
let b = tt.stop_idx_of(&S::B);
let profile = tt
.query()
.from(&[(a, Duration::ZERO)])
.to(&[(b, Duration::ZERO)])
.max_transfers(3)
.depart_in_window([
SecondOfDay(0),
SecondOfDay(5),
SecondOfDay(10),
SecondOfDay(15),
SecondOfDay(20),
])
.run();
let mut points: Vec<(crate::SecondOfDay, crate::SecondOfDay)> = profile
.iter()
.map(|p| (p.depart, p.journey.arrival()))
.collect();
points.sort();
assert_eq!(
points,
vec![
(SecondOfDay(0), SecondOfDay(10)),
(SecondOfDay(10), SecondOfDay(20)),
(SecondOfDay(20), SecondOfDay(30))
]
);
}
#[test]
fn arrival_and_walk_returns_pareto_front() {
use crate::labels::ArrivalAndWalk;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum S {
A,
X,
Y,
T,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum R {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Tr {
T1,
T2,
}
let tt = SimpleTimetable::new()
.route(
R::R1,
&[S::A, S::X],
&[(
Tr::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
)
.route(
R::R2,
&[S::A, S::Y],
&[(
Tr::T2,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(20), SecondOfDay(20)),
],
)],
)
.footpath(S::X, S::T)
.transfer_time(S::X, S::T, Duration(5))
.footpath(S::Y, S::T)
.transfer_time(S::Y, S::T, Duration(1));
let a = tt.stop_idx_of(&S::A);
let t = tt.stop_idx_of(&S::T);
let arrival_only = tt
.query()
.from(&[(a, Duration::ZERO)])
.to(&[(t, Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(arrival_only.len(), 1);
assert_eq!(arrival_only[0].arrival(), SecondOfDay(15));
let pareto: Vec<crate::Journey<ArrivalAndWalk>> = tt
.query_with_label::<ArrivalAndWalk>()
.from(a)
.to(t)
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(
pareto.len(),
2,
"expected Pareto front of two journeys, got {}: {:?}",
pareto.len(),
pareto.iter().map(|j| j.label).collect::<Vec<_>>(),
);
let mut labels: Vec<_> = pareto.iter().map(|j| j.label).collect();
labels.sort_by_key(|l| l.arrival);
assert_eq!(labels[0].arrival, SecondOfDay(15));
assert_eq!(labels[0].walk_time, Duration(5));
assert_eq!(labels[1].arrival, SecondOfDay(21));
assert_eq!(labels[1].walk_time, Duration(1));
}
#[test]
fn with_timing_recovers_per_leg_trip_and_times() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum S {
A,
B,
C,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum R {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Tr {
T1Early,
T1Late,
T2,
}
let tt = SimpleTimetable::new()
.route(
R::R1,
&[S::A, S::B],
&[
(
Tr::T1Early,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
),
(
Tr::T1Late,
&[
(SecondOfDay(50), SecondOfDay(50)),
(SecondOfDay(60), SecondOfDay(60)),
],
),
],
)
.route(
R::R2,
&[S::B, S::C],
&[(
Tr::T2,
&[
(SecondOfDay(12), SecondOfDay(12)),
(SecondOfDay(25), SecondOfDay(25)),
],
)],
);
let a = tt.stop_idx_of(&S::A);
let b = tt.stop_idx_of(&S::B);
let c = tt.stop_idx_of(&S::C);
let journeys = tt
.query()
.from(&[(a, Duration::ZERO)])
.to(&[(c, Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(journeys.len(), 1);
let j = &journeys[0];
assert_eq!(j.arrival(), SecondOfDay(25));
let timed = j
.with_timing(&tt, SecondOfDay(0), Duration::ZERO)
.expect("plan must reconstruct");
assert_eq!(timed.len(), 2);
let l1 = &timed[0];
assert_eq!(l1.route, tt.route_idx_of(&R::R1));
assert_eq!(l1.board, a);
assert_eq!(l1.alight, b);
assert_eq!(l1.trip, tt.trip_idx_of(&Tr::T1Early));
assert_eq!(l1.depart, SecondOfDay(0));
assert_eq!(l1.arrive, SecondOfDay(10));
let l2 = &timed[1];
assert_eq!(l2.route, tt.route_idx_of(&R::R2));
assert_eq!(l2.board, b);
assert_eq!(l2.alight, c);
assert_eq!(l2.trip, tt.trip_idx_of(&Tr::T2));
assert_eq!(l2.depart, SecondOfDay(12));
assert_eq!(l2.arrive, SecondOfDay(25));
}
#[test]
fn with_timing_handles_one_hop_walking_transfer() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum S {
A,
B,
C,
D,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum R {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Tr {
T1,
T2,
}
let tt = SimpleTimetable::new()
.route(
R::R1,
&[S::A, S::B],
&[(
Tr::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
)
.route(
R::R2,
&[S::C, S::D],
&[(
Tr::T2,
&[
(SecondOfDay(20), SecondOfDay(20)),
(SecondOfDay(30), SecondOfDay(30)),
],
)],
)
.footpath(S::B, S::C)
.transfer_time(S::B, S::C, Duration(5));
let a = tt.stop_idx_of(&S::A);
let b = tt.stop_idx_of(&S::B);
let c = tt.stop_idx_of(&S::C);
let d = tt.stop_idx_of(&S::D);
let journeys = tt
.query()
.from(&[(a, Duration::ZERO)])
.to(&[(d, Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(journeys.len(), 1);
let j = &journeys[0];
let timed = j
.with_timing(&tt, SecondOfDay(0), Duration::ZERO)
.expect("plan must reconstruct");
assert_eq!(timed.len(), 2);
let l1 = &timed[0];
assert_eq!(l1.alight, b, "leg 1 alights at B");
let l2 = &timed[1];
assert_eq!(l2.board, c, "leg 2 boards at C, not B (walking transfer)");
assert_eq!(l2.alight, d);
assert_eq!(l2.depart, SecondOfDay(20));
assert_eq!(l2.arrive, SecondOfDay(30));
}
fn pool_test_timetable() -> SimpleTimetable<char, u32, u32> {
SimpleTimetable::new()
.route(
1,
&['A', 'B', 'C'],
&[(
10,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(60), SecondOfDay(60)),
(SecondOfDay(120), SecondOfDay(120)),
],
)],
)
.route(
2,
&['C', 'D'],
&[(
20,
&[
(SecondOfDay(150), SecondOfDay(150)),
(SecondOfDay(210), SecondOfDay(210)),
],
)],
)
}
#[test]
fn pool_checkout_matches_single_cache_result() {
let tt = pool_test_timetable();
let a = tt.stop_idx_of(&'A');
let d = tt.stop_idx_of(&'D');
let baseline = tt
.query()
.from(a)
.to(d)
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
let pool = RaptorCachePool::for_timetable(&tt);
let mut cache = pool.checkout();
let pooled = tt
.query()
.from(a)
.to(d)
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run_with_cache(&mut cache);
assert_eq!(baseline.len(), pooled.len());
for (a, b) in baseline.iter().zip(&pooled) {
assert_eq!(a.plan, b.plan);
assert_eq!(a.arrival(), b.arrival());
}
}
#[test]
fn pool_reuses_caches_across_checkouts() {
let tt = pool_test_timetable();
let pool: RaptorCachePool = RaptorCachePool::for_timetable(&tt);
assert!(format!("{:?}", pool).contains("idle_caches: 0"));
{
let _c = pool.checkout(); }
assert!(format!("{:?}", pool).contains("idle_caches: 1"));
{
let _c1 = pool.checkout(); let _c2 = pool.checkout(); }
assert!(format!("{:?}", pool).contains("idle_caches: 2"));
}
#[test]
fn pool_is_sync_across_threads() {
let tt = pool_test_timetable();
let a = tt.stop_idx_of(&'A');
let d = tt.stop_idx_of(&'D');
let pool: RaptorCachePool = RaptorCachePool::for_timetable(&tt);
let baseline_arrival = tt
.query()
.from(a)
.to(d)
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run()
.into_iter()
.map(|j| j.arrival())
.collect::<Vec<_>>();
std::thread::scope(|s| {
let mut handles = Vec::new();
for _ in 0..8 {
let pool = &pool;
let tt = &tt;
let baseline = &baseline_arrival;
handles.push(s.spawn(move || {
let mut cache = pool.checkout();
let arrivals = tt
.query()
.from(a)
.to(d)
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run_with_cache(&mut cache)
.into_iter()
.map(|j| j.arrival())
.collect::<Vec<_>>();
assert_eq!(&arrivals, baseline);
}));
}
for h in handles {
h.join().unwrap();
}
});
let idle_caches = format!("{:?}", pool);
assert!(
!idle_caches.contains("idle_caches: 0"),
"pool should have returned caches after threads finished: {}",
idle_caches
);
}
#[cfg(feature = "parallel")]
#[test]
fn parallel_range_query_matches_serial() {
let tt = pool_test_timetable();
let a = tt.stop_idx_of(&'A');
let d = tt.stop_idx_of(&'D');
let departures: Vec<SecondOfDay> = (0..=120).step_by(15).map(SecondOfDay).collect();
let serial = tt
.query()
.from(a)
.to(d)
.max_transfers(3)
.depart_in_window(departures.iter().copied())
.run();
let parallel = tt
.query()
.from(a)
.to(d)
.max_transfers(3)
.depart_in_window(departures.iter().copied())
.run_par();
assert_eq!(serial.len(), parallel.len(), "front sizes differ");
for (s, p) in serial.iter().zip(¶llel) {
assert_eq!(s.depart, p.depart);
assert_eq!(s.journey.plan, p.journey.plan);
assert_eq!(s.journey.arrival(), p.journey.arrival());
}
let pool = RaptorCachePool::for_timetable(&tt);
let parallel_pooled = tt
.query()
.from(a)
.to(d)
.max_transfers(3)
.depart_in_window(departures.iter().copied())
.run_with_pool(&pool);
assert_eq!(serial.len(), parallel_pooled.len());
for (s, p) in serial.iter().zip(¶llel_pooled) {
assert_eq!(s.depart, p.depart);
assert_eq!(s.journey.plan, p.journey.plan);
}
}
#[test]
fn newly_active_stops_marks_only_in_window() {
use crate::algorithm::range::newly_active_stops_into;
use fixedbitset::FixedBitSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum S {
A,
B,
C,
D,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum R {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Tr {
T1,
T2,
T3,
T4,
T5,
T6,
}
let tt = SimpleTimetable::new()
.route(
R::R1,
&[S::A, S::B],
&[
(
Tr::T1,
&[
(SecondOfDay(100), SecondOfDay(100)),
(SecondOfDay(110), SecondOfDay(110)),
],
),
(
Tr::T2,
&[
(SecondOfDay(200), SecondOfDay(200)),
(SecondOfDay(210), SecondOfDay(210)),
],
),
(
Tr::T3,
&[
(SecondOfDay(300), SecondOfDay(300)),
(SecondOfDay(310), SecondOfDay(310)),
],
),
],
)
.route(
R::R2,
&[S::C, S::D],
&[
(
Tr::T4,
&[
(SecondOfDay(150), SecondOfDay(150)),
(SecondOfDay(160), SecondOfDay(160)),
],
),
(
Tr::T5,
&[
(SecondOfDay(250), SecondOfDay(250)),
(SecondOfDay(260), SecondOfDay(260)),
],
),
(
Tr::T6,
&[
(SecondOfDay(350), SecondOfDay(350)),
(SecondOfDay(360), SecondOfDay(360)),
],
),
],
);
let mut marked = FixedBitSet::with_capacity(tt.n_stops());
newly_active_stops_into(&tt, SecondOfDay(180), SecondOfDay(270), &mut marked, false);
assert!(marked.contains(tt.stop_idx_of(&S::A).idx()));
assert!(marked.contains(tt.stop_idx_of(&S::B).idx()));
assert!(marked.contains(tt.stop_idx_of(&S::C).idx()));
assert!(marked.contains(tt.stop_idx_of(&S::D).idx()));
let mut marked2 = FixedBitSet::with_capacity(tt.n_stops());
newly_active_stops_into(&tt, SecondOfDay(500), SecondOfDay(500), &mut marked2, false);
assert_eq!(marked2.count_ones(..), 0);
let mut marked3 = FixedBitSet::with_capacity(tt.n_stops());
newly_active_stops_into(&tt, SecondOfDay(120), SecondOfDay(140), &mut marked3, false);
assert_eq!(marked3.count_ones(..), 0);
let mut marked4 = FixedBitSet::with_capacity(tt.n_stops());
newly_active_stops_into(&tt, SecondOfDay(200), SecondOfDay(250), &mut marked4, false);
assert!(marked4.contains(tt.stop_idx_of(&S::A).idx()));
assert!(marked4.contains(tt.stop_idx_of(&S::B).idx()));
assert!(!marked4.contains(tt.stop_idx_of(&S::C).idx()));
assert!(!marked4.contains(tt.stop_idx_of(&S::D).idx()));
let mut marked5 = FixedBitSet::with_capacity(tt.n_stops());
newly_active_stops_into(&tt, SecondOfDay(300), SecondOfDay(350), &mut marked5, false);
assert!(marked5.contains(tt.stop_idx_of(&S::A).idx()));
assert!(marked5.contains(tt.stop_idx_of(&S::B).idx()));
assert!(!marked5.contains(tt.stop_idx_of(&S::C).idx()));
assert!(!marked5.contains(tt.stop_idx_of(&S::D).idx()));
let mut marked6 = FixedBitSet::with_capacity(tt.n_stops());
marked6.insert(tt.stop_idx_of(&S::A).idx());
newly_active_stops_into(&tt, SecondOfDay(500), SecondOfDay(500), &mut marked6, false);
assert!(marked6.contains(tt.stop_idx_of(&S::A).idx()));
}
#[test]
fn pickup_disallowed_at_boarding_stop_yields_no_journey() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
}
use Route::*;
use Stop::*;
use Trip::*;
let tt = SimpleTimetable::new()
.route(
R1,
&[A, B],
&[(
T1,
&[
(SecondOfDay(100), SecondOfDay(100)),
(SecondOfDay(200), SecondOfDay(200)),
],
)],
)
.no_pickup_at(T1, 0);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&B), Duration::ZERO)])
.max_transfers(1)
.depart_at(SecondOfDay(0))
.run();
assert!(
journeys.iter().all(|j| j.plan.is_empty()),
"no transit journey should exist when pickup is forbidden at the only boarding stop, got {journeys:?}"
);
}
#[test]
fn drop_off_disallowed_at_target_yields_no_journey_to_that_stop() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
C,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
}
use Route::*;
use Stop::*;
use Trip::*;
let tt = SimpleTimetable::new()
.route(
R1,
&[A, B, C],
&[(
T1,
&[
(SecondOfDay(100), SecondOfDay(100)),
(SecondOfDay(200), SecondOfDay(200)),
(SecondOfDay(300), SecondOfDay(300)),
],
)],
)
.no_drop_off_at(T1, 1);
let journeys_to_b = tt
.query()
.from(&[(tt.stop_idx_of(&A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&B), Duration::ZERO)])
.max_transfers(1)
.depart_at(SecondOfDay(0))
.run();
assert!(
journeys_to_b.iter().all(|j| j.plan.is_empty()),
"no transit journey should reach B when drop-off there is forbidden, got {journeys_to_b:?}"
);
let journeys_to_c = tt
.query()
.from(&[(tt.stop_idx_of(&A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&C), Duration::ZERO)])
.max_transfers(1)
.depart_at(SecondOfDay(0))
.run();
let best = journeys_to_c
.iter()
.filter(|j| !j.plan.is_empty())
.min_by_key(|j| j.arrival())
.expect("A -> C should still find a journey");
assert_eq!(best.arrival(), SecondOfDay(300));
}
#[test]
fn get_earliest_trip_skips_to_next_pickup_allowed_trip() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1Early,
T2Late,
}
use Route::*;
use Stop::*;
use Trip::*;
let tt = SimpleTimetable::new()
.route(
R1,
&[A, B],
&[
(
T1Early,
&[
(SecondOfDay(100), SecondOfDay(100)),
(SecondOfDay(200), SecondOfDay(200)),
],
),
(
T2Late,
&[
(SecondOfDay(300), SecondOfDay(300)),
(SecondOfDay(400), SecondOfDay(400)),
],
),
],
)
.no_pickup_at(T1Early, 0);
let journeys = tt
.query()
.from(&[(tt.stop_idx_of(&A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&B), Duration::ZERO)])
.max_transfers(1)
.depart_at(SecondOfDay(0))
.run();
let best = journeys
.iter()
.filter(|j| !j.plan.is_empty())
.min_by_key(|j| j.arrival())
.expect("a journey using T2Late should exist");
assert_eq!(
best.arrival(),
SecondOfDay(400),
"should board T2Late (arr 400), not T1Early (arr 200)",
);
}
#[test]
fn require_wheelchair_skips_inaccessible_trip() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1Inaccessible,
T2Accessible,
}
use Route::*;
use Stop::*;
use Trip::*;
let tt = SimpleTimetable::new()
.route(
R1,
&[A, B],
&[
(
T1Inaccessible,
&[
(SecondOfDay(100), SecondOfDay(100)),
(SecondOfDay(200), SecondOfDay(200)),
],
),
(
T2Accessible,
&[
(SecondOfDay(300), SecondOfDay(300)),
(SecondOfDay(400), SecondOfDay(400)),
],
),
],
)
.no_wheelchair_on_trip(T1Inaccessible);
let baseline = tt
.query()
.from(&[(tt.stop_idx_of(&A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&B), Duration::ZERO)])
.max_transfers(1)
.depart_at(SecondOfDay(0))
.run();
let best_baseline = baseline.iter().min_by_key(|j| j.arrival()).unwrap();
assert_eq!(best_baseline.arrival(), SecondOfDay(200));
let filtered = tt
.query()
.from(&[(tt.stop_idx_of(&A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&B), Duration::ZERO)])
.max_transfers(1)
.require_wheelchair_accessible()
.depart_at(SecondOfDay(0))
.run();
let best_filtered = filtered
.iter()
.filter(|j| !j.plan.is_empty())
.min_by_key(|j| j.arrival())
.expect("an accessible journey should exist");
assert_eq!(best_filtered.arrival(), SecondOfDay(400));
}
#[test]
fn require_wheelchair_skips_inaccessible_alighting_stop() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
C,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
}
use Route::*;
use Stop::*;
use Trip::*;
let tt = SimpleTimetable::new()
.route(
R1,
&[A, B, C],
&[(
T1,
&[
(SecondOfDay(100), SecondOfDay(100)),
(SecondOfDay(200), SecondOfDay(200)),
(SecondOfDay(300), SecondOfDay(300)),
],
)],
)
.no_wheelchair_at_stop(B);
let to_b = tt
.query()
.from(&[(tt.stop_idx_of(&A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&B), Duration::ZERO)])
.max_transfers(1)
.require_wheelchair_accessible()
.depart_at(SecondOfDay(0))
.run();
assert!(
to_b.iter().all(|j| j.plan.is_empty()),
"no transit journey should reach B when it's wheelchair-inaccessible, got {to_b:?}"
);
let to_c = tt
.query()
.from(&[(tt.stop_idx_of(&A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&C), Duration::ZERO)])
.max_transfers(1)
.require_wheelchair_accessible()
.depart_at(SecondOfDay(0))
.run();
let best = to_c
.iter()
.filter(|j| !j.plan.is_empty())
.min_by_key(|j| j.arrival())
.expect("A -> C should still find a journey");
assert_eq!(best.arrival(), SecondOfDay(300));
}
#[test]
fn require_wheelchair_returns_no_journey_when_only_trip_is_inaccessible() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
}
use Route::*;
use Stop::*;
use Trip::*;
let tt = SimpleTimetable::new()
.route(
R1,
&[A, B],
&[(
T1,
&[
(SecondOfDay(100), SecondOfDay(100)),
(SecondOfDay(200), SecondOfDay(200)),
],
)],
)
.no_wheelchair_on_trip(T1);
let filtered = tt
.query()
.from(&[(tt.stop_idx_of(&A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&B), Duration::ZERO)])
.max_transfers(1)
.require_wheelchair_accessible()
.depart_at(SecondOfDay(0))
.run();
assert!(
filtered.iter().all(|j| j.plan.is_empty()),
"no transit journey should exist when the only trip is inaccessible, got {filtered:?}"
);
}
#[test]
fn arrival_and_fare_returns_pareto_front() {
use crate::labels::{ArrivalAndFare, FareTable};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum S {
A,
X,
Y,
T,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum R {
Fast,
Slow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Tr {
T1,
T2,
}
let tt = SimpleTimetable::new()
.route(
R::Fast,
&[S::A, S::X],
&[(
Tr::T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(10), SecondOfDay(10)),
],
)],
)
.route(
R::Slow,
&[S::A, S::Y],
&[(
Tr::T2,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(20), SecondOfDay(20)),
],
)],
)
.footpath(S::X, S::T)
.transfer_time(S::X, S::T, Duration(5))
.footpath(S::Y, S::T)
.transfer_time(S::Y, S::T, Duration(1));
let fast_route = tt.route_idx_of(&R::Fast);
let mut per_route = HashMap::new();
per_route.insert(fast_route, 500u32);
let fares = FareTable { per_route };
let journeys: Vec<Journey<ArrivalAndFare>> = tt
.query_with_label::<ArrivalAndFare>()
.with_context(fares)
.from(&[(tt.stop_idx_of(&S::A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&S::T), Duration::ZERO)])
.max_transfers(3)
.depart_at(SecondOfDay(0))
.run();
let by_arrival: std::collections::BTreeMap<u32, u32> = journeys
.iter()
.filter(|j| !j.plan.is_empty())
.map(|j| (j.label.arrival.0, j.label.fare))
.collect();
assert!(
by_arrival.contains_key(&15),
"fast+expensive journey missing: {by_arrival:?}",
);
assert!(
by_arrival.contains_key(&21),
"slow+free journey missing: {by_arrival:?}",
);
assert_eq!(by_arrival[&15], 500);
assert_eq!(by_arrival[&21], 0);
}
#[test]
fn arrival_and_fare_dominated_route_dropped() {
use crate::labels::{ArrivalAndFare, FareTable};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R1,
R2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T1,
T2,
}
use Route::*;
use Stop::*;
use Trip::*;
let tt = SimpleTimetable::new()
.route(
R1,
&[A, B],
&[(
T1,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(100), SecondOfDay(100)),
],
)],
)
.route(
R2,
&[A, B],
&[(
T2,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(150), SecondOfDay(150)),
],
)],
);
let r2_route = tt.route_idx_of(&R2);
let mut per_route = HashMap::new();
per_route.insert(r2_route, 100u32);
let fares = FareTable { per_route };
let journeys = tt
.query_with_label::<ArrivalAndFare>()
.with_context(fares)
.from(&[(tt.stop_idx_of(&A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&B), Duration::ZERO)])
.max_transfers(1)
.depart_at(SecondOfDay(0))
.run();
let by_trips: Vec<&Journey<ArrivalAndFare>> =
journeys.iter().filter(|j| j.plan.len() == 1).collect();
assert_eq!(by_trips.len(), 1, "R2 should be dominated and dropped");
let kept = by_trips[0];
assert_eq!(kept.label.arrival.0, 100);
assert_eq!(kept.label.fare, 0);
}
#[test]
fn arrival_and_fare_default_ctx_is_zero_fare() {
use crate::labels::ArrivalAndFare;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Stop {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Route {
R,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Trip {
T,
}
use Route::*;
use Stop::*;
use Trip::*;
let tt = SimpleTimetable::new().route(
R,
&[A, B],
&[(
T,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(100), SecondOfDay(100)),
],
)],
);
let journeys = tt
.query_with_label::<ArrivalAndFare>()
.from(&[(tt.stop_idx_of(&A), Duration::ZERO)])
.to(&[(tt.stop_idx_of(&B), Duration::ZERO)])
.max_transfers(1)
.depart_at(SecondOfDay(0))
.run();
let kept = journeys
.iter()
.find(|j| j.plan.len() == 1)
.expect("a one-trip journey should exist");
assert_eq!(kept.label.arrival.0, 100);
assert_eq!(kept.label.fare, 0, "no FareTable -> fare should be zero");
}
#[test]
fn multi_source_target_equal_to_origin_finds_other_origins_journey() {
use crate::Journey;
const A: u8 = 0;
const B: u8 = 1;
let tt = SimpleTimetable::new().route(
0u8,
&[A, B],
&[(
0u16,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(1), SecondOfDay(1)),
],
)],
);
let s_a = tt.stop_idx_of(&A);
let s_b = tt.stop_idx_of(&B);
let origins = [(s_a, Duration::ZERO), (s_b, Duration(2))];
let targets = [(s_b, Duration::ZERO)];
let journeys: Vec<Journey<crate::ArrivalTime>> = tt
.query()
.from(origins.as_slice())
.to(targets.as_slice())
.max_transfers(1)
.depart_at(SecondOfDay(0))
.run();
assert_eq!(journeys.len(), 1, "expected one journey, got {journeys:#?}");
let j = &journeys[0];
assert_eq!(j.origin, s_a);
assert_eq!(j.target, s_b);
assert_eq!(j.label.0.0, 1);
assert_eq!(j.plan.len(), 1);
}
#[test]
fn wheelchair_query_finds_accessible_sibling_at_same_departure() {
use crate::Journey;
const A: u8 = 0;
const B: u8 = 1;
let tt = SimpleTimetable::new()
.route(
0u8,
&[A, B],
&[
(
0u16,
&[
(SecondOfDay(50), SecondOfDay(50)),
(SecondOfDay(51), SecondOfDay(51)),
],
),
(
1u16,
&[
(SecondOfDay(50), SecondOfDay(50)),
(SecondOfDay(51), SecondOfDay(51)),
],
),
],
)
.no_wheelchair_on_trip(0u16);
let s_a = tt.stop_idx_of(&A);
let s_b = tt.stop_idx_of(&B);
let journeys: Vec<Journey<crate::ArrivalTime>> = tt
.query()
.from(&[(s_a, Duration::ZERO)])
.to(&[(s_b, Duration::ZERO)])
.max_transfers(1)
.require_wheelchair_accessible()
.depart_at(SecondOfDay(0))
.run();
assert_eq!(journeys.len(), 1, "expected one journey, got {journeys:#?}");
assert_eq!(journeys[0].label.0.0, 51);
}
#[test]
fn ffi_entry_points_accept_dyn_timetable() {
use crate::ffi;
use crate::labels::FareTable;
const A: u8 = 0;
const B: u8 = 1;
let tt = SimpleTimetable::new().route(
0u8,
&[A, B],
&[(
0u16,
&[
(SecondOfDay(0), SecondOfDay(0)),
(SecondOfDay(60), SecondOfDay(60)),
],
)],
);
let s_a = tt.stop_idx_of(&A);
let s_b = tt.stop_idx_of(&B);
let origins = [(s_a, Duration::ZERO)];
let targets = [(s_b, Duration::ZERO)];
let arrival_concrete = ffi::run_arrival(&tt, &origins, &targets, 1, SecondOfDay(0), false);
let walk_concrete = ffi::run_walk(&tt, &origins, &targets, 1, SecondOfDay(0), false);
let fare_concrete = ffi::run_fare(
&tt,
&FareTable::default(),
&origins,
&targets,
1,
SecondOfDay(0),
false,
);
let dyn_tt: &dyn crate::Timetable = &tt;
let arrival_dyn = ffi::run_arrival(dyn_tt, &origins, &targets, 1, SecondOfDay(0), false);
let walk_dyn = ffi::run_walk(dyn_tt, &origins, &targets, 1, SecondOfDay(0), false);
let fare_dyn = ffi::run_fare(
dyn_tt,
&FareTable::default(),
&origins,
&targets,
1,
SecondOfDay(0),
false,
);
assert_eq!(arrival_concrete.len(), 1);
assert_eq!(arrival_concrete[0].label.0.0, 60);
assert_eq!(arrival_dyn.len(), 1);
assert_eq!(arrival_dyn[0].label.0.0, 60);
assert_eq!(walk_concrete.len(), 1);
assert_eq!(walk_concrete[0].label.arrival.0, 60);
assert_eq!(walk_dyn.len(), 1);
assert_eq!(fare_concrete.len(), 1);
assert_eq!(fare_concrete[0].label.fare, 0);
assert_eq!(fare_dyn.len(), 1);
}
#[test]
fn builder_accepts_ergonomic_input_types() {
use crate::Journey;
use crate::Transfers;
use jiff::civil::time;
const A: u8 = 0;
const B: u8 = 1;
let tt = SimpleTimetable::new().route(
0u8,
&[A, B],
&[(
0u16,
&[
(SecondOfDay::hms(9, 0, 0), SecondOfDay::hms(9, 0, 0)),
(SecondOfDay::hms(9, 1, 0), SecondOfDay::hms(9, 1, 0)),
],
)],
);
let s_a = tt.stop_idx_of(&A);
let s_b = tt.stop_idx_of(&B);
let baseline: Vec<Journey<crate::ArrivalTime>> = tt
.query()
.from(s_a)
.to(s_b)
.max_transfers(1u8)
.depart_at(SecondOfDay::hms(9, 0, 0))
.run();
let typed: Vec<Journey<crate::ArrivalTime>> = tt
.query()
.from(s_a)
.to(s_b)
.max_transfers(Transfers(1))
.depart_at(time(9, 0, 0, 0))
.run();
assert_eq!(baseline.len(), typed.len());
assert_eq!(baseline[0].label, typed[0].label);
assert_eq!(baseline[0].plan, typed[0].plan);
}