vrp_cli/extensions/import/
csv.rs1#[cfg(test)]
3#[path = "../../../tests/unit/extensions/import/csv_test.rs"]
4mod csv_test;
5
6pub use self::actual::read_csv_problem;
7
8#[cfg(feature = "csv-format")]
9mod actual {
10 extern crate csv;
11 extern crate serde;
12
13 use serde::Deserialize;
14 use std::collections::{HashMap, HashSet};
15 use std::error::Error;
16 use std::io::{BufReader, Read};
17 use vrp_core::prelude::Float;
18 use vrp_pragmatic::format::problem::*;
19 use vrp_pragmatic::format::{FormatError, Location};
20
21 #[derive(Debug, Deserialize)]
22 #[serde(rename_all = "UPPERCASE")]
23 struct CsvJob {
24 id: String,
25 lat: f64,
26 lng: f64,
27 demand: i32,
28 duration: usize,
29 tw_start: Option<String>,
30 tw_end: Option<String>,
31 }
32
33 #[derive(Debug, Deserialize)]
34 #[serde(rename_all = "UPPERCASE")]
35 struct CsvVehicle {
36 id: String,
37 lat: f64,
38 lng: f64,
39 capacity: i32,
40 tw_start: String,
41 tw_end: String,
42 amount: usize,
43 profile: String,
44 }
45
46 fn read_csv_entries<T, R: Read>(reader: BufReader<R>) -> Result<Vec<T>, Box<dyn Error>>
47 where
48 for<'de> T: Deserialize<'de>,
49 {
50 let mut reader = csv::Reader::from_reader(reader);
51 let mut entries = vec![];
52
53 for entry in reader.deserialize() {
54 entries.push(entry?);
55 }
56
57 Ok(entries)
58 }
59
60 fn parse_tw(start: Option<String>, end: Option<String>) -> Option<Vec<String>> {
61 match (start, end) {
62 (Some(start), Some(end)) => Some(vec![start, end]),
63 _ => None,
64 }
65 }
66
67 fn read_jobs<R: Read>(reader: BufReader<R>) -> Result<Vec<Job>, Box<dyn Error>> {
68 let get_task = |job: &CsvJob| JobTask {
69 places: vec![JobPlace {
70 location: Location::Coordinate { lat: job.lat, lng: job.lng },
71 duration: job.duration as Float,
72 times: parse_tw(job.tw_start.clone(), job.tw_end.clone()).map(|tw| vec![tw]),
73 tag: None,
74 }],
75 demand: if job.demand != 0 { Some(vec![job.demand.abs()]) } else { None },
76 order: None,
77 };
78
79 let get_tasks = |jobs: &Vec<&CsvJob>, filter: Box<dyn Fn(&CsvJob) -> bool>| {
80 let tasks = jobs.iter().filter(|j| (filter)(j)).map(|job| get_task(job)).collect::<Vec<_>>();
81 if tasks.is_empty() {
82 None
83 } else {
84 Some(tasks)
85 }
86 };
87
88 let jobs = read_csv_entries::<CsvJob, _>(reader)?
89 .iter()
90 .fold(HashMap::<_, Vec<_>>::new(), |mut acc, job| {
91 acc.entry(&job.id).or_default().push(job);
92 acc
93 })
94 .into_iter()
95 .map(|(job_id, tasks)| Job {
96 id: job_id.clone(),
97 pickups: get_tasks(&tasks, Box::new(|j| j.demand > 0)),
98 deliveries: get_tasks(&tasks, Box::new(|j| j.demand < 0)),
99 replacements: None,
100 services: get_tasks(&tasks, Box::new(|j| j.demand == 0)),
101 skills: None,
102 value: None,
103 group: None,
104 compatibility: None,
105 })
106 .collect();
107
108 Ok(jobs)
109 }
110
111 fn read_vehicles<R: Read>(reader: BufReader<R>) -> Result<Vec<VehicleType>, Box<dyn Error>> {
112 let vehicles = read_csv_entries::<CsvVehicle, _>(reader)?
113 .into_iter()
114 .map(|vehicle| {
115 let depot_location = Location::Coordinate { lat: vehicle.lat, lng: vehicle.lng };
116
117 VehicleType {
118 type_id: vehicle.id.clone(),
119 vehicle_ids: (1..=vehicle.amount).map(|seq| format!("{}_{}", vehicle.profile, seq)).collect(),
120 profile: VehicleProfile { matrix: vehicle.profile, scale: None },
121 costs: VehicleCosts { fixed: Some(25.), distance: 0.0002, time: 0.005 },
122 shifts: vec![VehicleShift {
123 start: ShiftStart {
124 earliest: vehicle.tw_start,
125 latest: None,
126 location: depot_location.clone(),
127 },
128 end: Some(ShiftEnd { earliest: None, latest: vehicle.tw_end, location: depot_location }),
129 breaks: None,
130 reloads: None,
131 recharges: None,
132 }],
133 capacity: vec![vehicle.capacity],
134 skills: None,
135 limits: None,
136 }
137 })
138 .collect();
139
140 Ok(vehicles)
141 }
142
143 fn create_format_error(entity: &str, error: Box<dyn Error>) -> FormatError {
144 FormatError::new_with_details(
145 "E0000".to_string(),
146 format!("cannot read {entity}"),
147 format!("check {entity} definition"),
148 format!("{error}",),
149 )
150 }
151
152 pub fn read_csv_problem<R1: Read, R2: Read>(
154 jobs_reader: BufReader<R1>,
155 vehicles_reader: BufReader<R2>,
156 ) -> Result<Problem, FormatError> {
157 let jobs = read_jobs(jobs_reader).map_err(|err| create_format_error("jobs", err))?;
158 let vehicles = read_vehicles(vehicles_reader).map_err(|err| create_format_error("vehicles", err))?;
159 let matrix_profile_names = vehicles.iter().map(|v| v.profile.matrix.clone()).collect::<HashSet<_>>();
160
161 Ok(Problem {
162 plan: Plan { jobs, relations: None, clustering: None },
163 fleet: Fleet {
164 vehicles,
165 profiles: matrix_profile_names.into_iter().map(|name| MatrixProfile { name, speed: None }).collect(),
166 resources: None,
167 },
168 objectives: None,
169 })
170 }
171}
172
173#[cfg(not(feature = "csv-format"))]
174mod actual {
175 use std::io::{BufReader, Read};
176 use vrp_pragmatic::format::problem::Problem;
177 use vrp_pragmatic::format::FormatError;
178
179 pub fn read_csv_problem<R1: Read, R2: Read>(
181 _jobs_reader: BufReader<R1>,
182 _vehicles_reader: BufReader<R2>,
183 ) -> Result<Problem, FormatError> {
184 unreachable!("csv-format feature is not included")
185 }
186}