vrp_cli/extensions/import/
csv.rs

1//! Import from a simple csv format logic.
2#[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    /// Reads problem from csv format.
153    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    /// A stub method for reading problem from csv format.
180    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}