///|
/// Errors returned when a benchmark instance is not structurally valid.
pub(all) suberror ParseError {
MissingHeader(String)
InvalidHeader(String)
InvalidNumber(String)
MissingSection(String)
InvalidRecord(String)
UnsupportedFormat(String)
} derive(Debug, Eq, ToJson)
///|
fn cleaned_lines(text : String) -> Array[String] {
let result : Array[String] = []
for line in text.split("\n") {
let cleaned = line.trim().to_owned()
if !cleaned.is_empty() {
result.push(cleaned)
}
}
result
}
///|
fn words(line : String) -> Array[String] {
let result : Array[String] = []
for word in line.split(" ") {
let cleaned = word.trim()
if !cleaned.is_empty() {
result.push(cleaned.to_owned())
}
}
result
}
///|
fn find_section(lines : Array[String], name : String) -> Int? {
for index, line in lines {
if line.trim().equal_to_string(name) {
return Some(index)
}
}
None
}
///|
fn header_value(lines : Array[String], name : String) -> String? {
for line in lines {
match line.split_once(":") {
None => ()
Some((key, value)) =>
if key.trim().equal_to_string(name) {
return Some(value.trim().to_owned())
}
}
}
None
}
///|
fn required_header(
lines : Array[String],
name : String,
) -> String raise ParseError {
match header_value(lines, name) {
Some(value) => value
None => raise ParseError::MissingHeader(name)
}
}
///|
fn parse_int(token : String) -> Int raise ParseError {
@strconv.from_str(token[:]) catch {
_ => raise ParseError::InvalidNumber(token)
}
}
///|
fn parse_double(token : String) -> Double raise ParseError {
@strconv.from_str(token[:]) catch {
_ => raise ParseError::InvalidNumber(token)
}
}
///|
fn parse_metric(value : String) -> @core.DistanceMetric raise ParseError {
match value.trim() {
"EUC_2D" => @core.DistanceMetric::RoundedEuclidean
"CEIL_2D" => @core.DistanceMetric::Euclidean
"MAN_2D" => @core.DistanceMetric::Manhattan
"EXPLICIT" => raise ParseError::UnsupportedFormat(value)
other => raise ParseError::UnsupportedFormat(other.to_owned())
}
}
///|
priv struct CoordinateRecord {
id : Int
x : Double
y : Double
}
///|
priv struct DemandRecord {
id : Int
demand : Double
}
///|
fn coordinate_records(
lines : Array[String],
start : Int,
end : Int,
) -> Array[CoordinateRecord] raise ParseError {
let records : Array[CoordinateRecord] = []
for index in (start + 1).. Array[DemandRecord] raise ParseError {
let records : Array[DemandRecord] = []
for index in (start + 1).. Int raise ParseError {
for index in (start + 1).. CoordinateRecord? {
for record in records {
if record.id == id {
return Some(record)
}
}
None
}
///|
fn demand_by_id(records : Array[DemandRecord], id : Int) -> Double? {
for record in records {
if record.id == id {
return Some(record.demand)
}
}
None
}
///|
fn optional_int_header(
lines : Array[String],
names : Array[String],
) -> Int? raise ParseError {
for name in names {
match header_value(lines, name) {
None => ()
Some(value) => return Some(parse_int(value))
}
}
None
}
///|
/// Parses the CVRPLIB/TSPLIB CVRP text format.
pub fn parse_cvrp(text : String) -> @core.VrpInstance raise ParseError {
let lines = cleaned_lines(text)
let name = required_header(lines, "NAME")
let kind = required_header(lines, "TYPE")
if kind != "CVRP" {
raise ParseError::UnsupportedFormat(kind)
}
let dimension = parse_int(required_header(lines, "DIMENSION"))
let capacity = parse_double(required_header(lines, "CAPACITY"))
let metric = parse_metric(required_header(lines, "EDGE_WEIGHT_TYPE"))
let coordinate_start = match find_section(lines, "NODE_COORD_SECTION") {
Some(index) => index
None => raise ParseError::MissingSection("NODE_COORD_SECTION")
}
let demand_start = match find_section(lines, "DEMAND_SECTION") {
Some(index) => index
None => raise ParseError::MissingSection("DEMAND_SECTION")
}
let depot_start = match find_section(lines, "DEPOT_SECTION") {
Some(index) => index
None => raise ParseError::MissingSection("DEPOT_SECTION")
}
if !(coordinate_start < demand_start && demand_start < depot_start) {
raise ParseError::InvalidRecord("section order")
}
let coordinates = coordinate_records(lines, coordinate_start, demand_start)
let demands = demand_records(lines, demand_start, depot_start)
if coordinates.length() != dimension || demands.length() != dimension {
raise ParseError::InvalidRecord("record count does not match DIMENSION")
}
let depot = depot_id(lines, depot_start, lines.length())
let depot_record = match coordinate_by_id(coordinates, depot) {
Some(record) => record
None => raise ParseError::InvalidRecord("depot coordinate")
}
let customers : Array[@core.Customer] = []
for coordinate in coordinates {
if coordinate.id == depot {
continue
}
let demand = match demand_by_id(demands, coordinate.id) {
Some(value) => value
None => raise ParseError::InvalidRecord("customer demand")
}
customers.push(
@core.Customer::new(coordinate.id, coordinate.x, coordinate.y, demand),
)
}
let fleet_size = match optional_int_header(lines, ["TRUCKS", "VEHICLES"]) {
Some(value) => value
None => dimension.max(1)
}
@core.make_instance_with_metric(
name,
@core.ProblemVariant::CVRP,
@core.Depot::new(depot_record.x, depot_record.y),
@core.VehicleProfile::new(capacity, fleet_size~),
customers,
metric,
)
}
///|
fn solomon_data_start(lines : Array[String]) -> Int raise ParseError {
for index, line in lines {
if line
.trim()
.equal_to_string(
"CUST NO. XCOORD. YCOORD. DEMAND READY TIME DUE DATE SERVICE TIME",
) {
return index + 1
}
}
raise ParseError::MissingSection("CUSTOMER")
}
///|
fn solomon_vehicle_data(
lines : Array[String],
) -> (Int, Double) raise ParseError {
for index, line in lines {
if line.trim().equal_to_string("NUMBER CAPACITY") &&
index + 1 < lines.length() {
let fields = words(lines[index + 1])
if fields.length() < 2 {
raise ParseError::InvalidRecord(lines[index + 1])
}
return (parse_int(fields[0]), parse_double(fields[1]))
}
}
raise ParseError::MissingSection("VEHICLE")
}
///|
/// Parses the Solomon 100-customer style VRPTW format.
pub fn parse_solomon(text : String) -> @core.VrpInstance raise ParseError {
let lines = cleaned_lines(text)
if lines.is_empty() {
raise ParseError::InvalidRecord("empty Solomon file")
}
let name = lines[0]
let (fleet_size, capacity) = solomon_vehicle_data(lines)
let start = solomon_data_start(lines)
let mut depot : @core.Depot? = None
let customers : Array[@core.Customer] = []
for index in start.. value
None => raise ParseError::InvalidRecord("missing Solomon depot")
}
@core.make_instance_with_metric(
name,
@core.ProblemVariant::VRPTW,
depot,
@core.VehicleProfile::new(
capacity,
fleet_size~,
max_route_duration=depot.due_time(),
),
customers,
@core.DistanceMetric::Euclidean,
)
}
///|
/// Parses either a CVRP or Solomon document based on its identifying headers.
pub fn parse_instance(text : String) -> @core.VrpInstance raise ParseError {
let lines = cleaned_lines(text)
if header_value(lines, "TYPE") is Some("CVRP") {
parse_cvrp(text)
} else {
parse_solomon(text)
}
}