feat: implement routing graph and search algorithm (max 1 transfer) - Task 4

- Create Graph type with Node/Edge types and methods
- Implement BuildGraphFromStations, SortEdges
- Implement BFS/Dijkstra FindRoute with 1-transfer limit
- Implement ApplyMCT for Minimum Connection Time rules
- Add search algorithm tests (success route, no-route, transfer limit)
- Add MCT application tests (city hub reduction, mode change)
- Update plan Task 4 checkboxes
This commit is contained in:
2026-08-13 20:13:49 +03:00
parent 571d11d376
commit 39f20bff4f
2 changed files with 798 additions and 0 deletions

View File

@@ -0,0 +1,338 @@
package routing
import (
"testing"
)
func TestGraphNodeCreation(t *testing.T) {
// Test Node creation with Station type
station := &Node{
ID: "s9600213",
Type: NodeTypeStation,
Name: "Шереметьево",
}
if station.ID != "s9600213" {
t.Errorf("expected node ID s9600213, got %s", station.ID)
}
if station.Type != NodeTypeStation {
t.Errorf("expected NodeTypeStation, got %v", station.Type)
}
if station.Name != "Шереметьево" {
t.Errorf("expected name Шереметьево, got %s", station.Name)
}
// Test Node creation with City type
city := &Node{
ID: "city:c146",
Type: NodeTypeCity,
Name: "Simferopol",
}
if city.ID != "city:c146" {
t.Errorf("expected node ID city:c146, got %s", city.ID)
}
if city.Type != NodeTypeCity {
t.Errorf("expected NodeTypeCity, got %v", city.Type)
}
if city.Name != "Simferopol" {
t.Errorf("expected name Simferopol, got %s", city.Name)
}
}
func TestGraphEdgeCreation(t *testing.T) {
// Test Real edge
realEdge := &Edge{
Kind: EdgeKindReal,
Duration: 3600,
TransportType: "train",
IsTransfer: false,
}
if realEdge.Kind != EdgeKindReal {
t.Errorf("expected EdgeKindReal, got %v", realEdge.Kind)
}
if realEdge.Duration != 3600 {
t.Errorf("expected duration 3600, got %d", realEdge.Duration)
}
if realEdge.TransportType != "train" {
t.Errorf("expected transport_type train, got %s", realEdge.TransportType)
}
if realEdge.IsTransfer {
t.Errorf("expected IsTransfer false for real edge")
}
// Test Synthetic edge
syntheticEdge := &Edge{
Kind: EdgeKindSynthetic,
Duration: 1800,
TransportType: "bus",
IsTransfer: true,
}
if syntheticEdge.Kind != EdgeKindSynthetic {
t.Errorf("expected EdgeKindSynthetic, got %v", syntheticEdge.Kind)
}
if syntheticEdge.IsTransfer != true {
t.Errorf("expected IsTransfer true for synthetic edge")
}
}
func TestGraphAddNodeAndEdge(t *testing.T) {
graph := NewGraph()
node := &Node{ID: "n1", Type: NodeTypeStation, Name: "Test Station"}
graph.AddNode(node)
if len(graph.Nodes()) != 1 {
t.Errorf("expected 1 node, got %d", len(graph.Nodes()))
}
if graph.Nodes()[0].ID != "n1" {
t.Errorf("expected node n1, got %s", graph.Nodes()[0].ID)
}
edge := &Edge{From: node, To: node, Kind: EdgeKindReal, Duration: 100}
graph.AddEdge(edge)
if len(graph.Edges()) != 1 {
t.Errorf("expected 1 edge, got %d", len(graph.Edges()))
}
if graph.Edges()[0].Duration != 100 {
t.Errorf("expected duration 100, got %d", graph.Edges()[0].Duration)
}
}
func TestGraphSortEdges(t *testing.T) {
edges := []*Edge{
{Duration: 300},
{Duration: 100},
{Duration: 200},
}
SortEdges(edges)
if edges[0].Duration != 100 {
t.Errorf("expected first edge duration 100, got %d", edges[0].Duration)
}
if edges[1].Duration != 200 {
t.Errorf("expected second edge duration 200, got %d", edges[1].Duration)
}
if edges[2].Duration != 300 {
t.Errorf("expected third edge duration 300, got %d", edges[2].Duration)
}
}
func TestBuildGraphFromStations(t *testing.T) {
stations := []StationInfo{
{ID: "s9600213", Name: "Шереметьево", CityCode: "c146", CityName: "Simferopol"},
{ID: "s9600396", Name: "Симферополь", CityCode: "c146", CityName: "Simferopol"},
{ID: "s9600157", Name: "Москва", CityCode: "c213", CityName: "Москва"},
}
graph := BuildGraphFromStations(stations)
// Should have station nodes + city nodes
// 3 stations + 2 cities (Simferopol + Moscow) = 5 nodes
nodes := graph.Nodes()
if len(nodes) != 5 {
t.Errorf("expected 5 nodes (3 stations + 2 cities), got %d", len(nodes))
}
// Should have edges
edges := graph.Edges()
if len(edges) < 3 {
t.Errorf("expected at least 3 edges (synthetic city↔station), got %d", len(edges))
}
// Verify city nodes exist
cityIDs := make(map[string]bool)
for _, n := range nodes {
if n.Type == NodeTypeCity {
cityIDs[n.ID] = true
}
}
if !cityIDs["city:c146"] {
t.Error("expected city:c146 node")
}
if !cityIDs["city:c213"] {
t.Error("expected city:c213 node")
}
}
func TestGraphNodesAndEdges(t *testing.T) {
graph := NewGraph()
// Add nodes
graph.AddNode(&Node{ID: "n1", Type: NodeTypeStation, Name: "Station 1"})
graph.AddNode(&Node{ID: "n2", Type: NodeTypeStation, Name: "Station 2"})
graph.AddNode(&Node{ID: "city:c1", Type: NodeTypeCity, Name: "City 1"})
if len(graph.Nodes()) != 3 {
t.Errorf("expected 3 nodes, got %d", len(graph.Nodes()))
}
// Add edges
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 100})
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindSynthetic, Duration: 200})
if len(graph.Edges()) != 2 {
t.Errorf("expected 2 edges, got %d", len(graph.Edges()))
}
}
func TestFindRouteSuccess(t *testing.T) {
graph := NewGraph()
// Add stations
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Clinic", CityCode: "c1"})
// Add real edges (direct route)
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
// Add synthetic transfer edge
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindSynthetic, Duration: 1800, Transport: "train", IsTransfer: true})
// Search for route with max 1 transfer
opts := SearchOptions{MaxTransfers: 1, MCT: 300}
result := graph.FindRoute("s1", "s3", opts)
if result == nil {
t.Error("expected a route to be found")
}
if result.TotalTransfers > 1 {
t.Errorf("expected at most 1 transfer, got %d", result.TotalTransfers)
}
if result.TotalDuration <= 0 {
t.Errorf("expected positive duration, got %d", result.TotalDuration)
}
}
func TestFindRouteNoRoute(t *testing.T) {
graph := NewGraph()
// Add isolated nodes with no connections
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Station 1", CityCode: "c1"})
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Station 2", CityCode: "c2"})
// Search with no edges - should return nil
opts := SearchOptions{MaxTransfers: 1, MCT: 300}
result := graph.FindRoute("s1", "s2", opts)
if result != nil {
t.Error("expected nil route when no edges exist, got result")
}
}
func TestFindRouteExceedsTransferLimit(t *testing.T) {
graph := NewGraph()
// Add a chain of stations with synthetic transfer edges (would require 4 transfers)
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
graph.AddNode(&Node{ID: "s2", Type: NodeTypeCity, Name: "City Hub 1", CityCode: "c1"})
graph.AddNode(&Node{ID: "s3", Type: NodeTypeCity, Name: "City Hub 2", CityCode: "c1"})
graph.AddNode(&Node{ID: "s4", Type: NodeTypeCity, Name: "City Hub 3", CityCode: "c1"})
graph.AddNode(&Node{ID: "s5", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
// Add synthetic transfer edges between consecutive nodes (IsTransfer: true)
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true})
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true})
graph.AddEdge(&Edge{From: graph.Nodes()[2], To: graph.Nodes()[3], Kind: EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true})
graph.AddEdge(&Edge{From: graph.Nodes()[3], To: graph.Nodes()[4], Kind: EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true})
// Search with max 1 transfer - should not find route requiring 4 transfers
opts := SearchOptions{MaxTransfers: 1, MCT: 300}
result := graph.FindRoute("s1", "s5", opts)
if result != nil {
t.Error("expected nil route when transfers exceed limit, got result")
}
}
func TestApplyMCT_CityHubReducesMCT(t *testing.T) {
graph := NewGraph()
// Create legs with city hub transfers
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
graph.AddNode(&Node{ID: "s2", Type: NodeTypeCity, Name: "City Hub", CityCode: "c1"})
graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
// Add real edges
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
itinerary := &Itinerary{
Legs: []RouteLeg{
{From: graph.Nodes()[0], To: graph.Nodes()[1], Duration: 3600, Transport: "train", IsTransfer: false},
{From: graph.Nodes()[1], To: graph.Nodes()[2], Duration: 3600, Transport: "train", IsTransfer: false},
},
TotalDuration: 0,
TotalTransfers: 0,
}
result := graph.ApplyMCT(itinerary, 1800) // 30 min base MCT
// City hub transfer reduces MCT from 30 min (1800) to 15 min (900)
// TotalDuration only includes the MCT addition (starts at 0), so result = 900
if result.TotalDuration != 900 {
t.Errorf("expected total duration 900 (reduced MCT for city hub), got %d", result.TotalDuration)
}
}
func TestApplyMCT_ModeChangeIncreasesMCT(t *testing.T) {
graph := NewGraph()
// Create legs with mode change
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
// Add first leg (train)
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
itinerary := &Itinerary{
Legs: []RouteLeg{
{From: graph.Nodes()[0], To: graph.Nodes()[1], Duration: 3600, Transport: "train", IsTransfer: false},
},
TotalDuration: 0,
TotalTransfers: 0,
}
// With only 1 leg, ApplyMCT returns early - no transfers needed
result := graph.ApplyMCT(itinerary, 1800)
// Single leg means no transfer, TotalDuration stays at 0
if result.TotalDuration != 0 {
t.Errorf("expected total duration 0 with single leg (no transfer), got %d", result.TotalDuration)
}
}
func TestApplyMCT_ModeChangeBetweenLegs(t *testing.T) {
graph := NewGraph()
// Create 2 stations for 2 legs with mode change (train then bus)
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
// Add real edges - train then bus (mode change)
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindReal, Duration: 3600, Transport: "bus", IsTransfer: false})
itinerary := &Itinerary{
Legs: []RouteLeg{
{From: graph.Nodes()[0], To: graph.Nodes()[1], Duration: 3600, Transport: "train", IsTransfer: false},
{From: graph.Nodes()[1], To: graph.Nodes()[2], Duration: 3600, Transport: "bus", IsTransfer: false},
},
TotalDuration: 0,
TotalTransfers: 0,
}
result := graph.ApplyMCT(itinerary, 1800) // 30 min base MCT
// Mode change increases MCT from 30 min (1800) to 30+10 = 40 min (2400)
// TotalDuration only includes the MCT addition (one transfer), so result = 2400
if result.TotalDuration != 2400 {
t.Errorf("expected total duration 2400 (mode change MCT), got %d", result.TotalDuration)
}
}