Fix remove.csv to include wishlist cars not best on any track

Previously, wishlist cars that weren't best on any track were silently
dropped from remove.csv. Now uses car_map as fallback to include all
non-ideal wishlist cars with available metadata, using an intermediate
RemoveEntry struct. Adds test coverage for this scenario.
This commit is contained in:
Jan-Peter von Hunnius 2026-05-26 08:52:42 +02:00
parent fc9f637c60
commit 4ecfc2d14d

View file

@ -320,30 +320,61 @@ pub fn write_wishlist_files(
)?;
// --- remove.csv (wishlist cars not in the ideal set) ---
// Single pass: collect found entries and count dropped ones together.
// Use a struct that can represent both best-cars entries (with count) and
// cars only in the car_map (without count info).
struct RemoveEntry {
brand: String,
only_name: String,
year: u16,
rq: u8,
normalized_config: String,
track_count: usize,
}
// Single pass: collect entries, using car_map as fallback for cars not in best data.
let mut dropped_count = 0usize;
let to_remove: Vec<_> = wishlist
let to_remove: Vec<RemoveEntry> = wishlist
.iter()
.filter(|guid| !ideal_guids.contains(*guid))
.filter_map(|guid| {
// First try: find the best entry for this car (if it's best on any track)
if let Some(keys) = counts.guid_to_rid.get(guid) {
// Pick the entry with the highest count (most impactful config)
keys.iter()
if let Some(key) = keys.iter()
.max_by_key(|key| counts.by_rid.get(key).map(|(_, c)| *c).unwrap_or(0))
.and_then(|key| counts.by_rid.get(key).cloned())
.or_else(|| {
dropped_count += 1;
None
})
} else {
dropped_count += 1;
None
{
if let Some((record, count)) = counts.by_rid.get(key) {
return Some(RemoveEntry {
brand: record.brand.clone(),
only_name: record.only_name.clone(),
year: record.year,
rq: record.rq,
normalized_config: record.normalized_config.clone(),
track_count: *count,
});
}
}
}
// Fallback: use car_map for cars not in best data (not best on any track)
if let Some(car) = counts.car_map.get(guid) {
return Some(RemoveEntry {
brand: car.brand.clone(),
only_name: car.only_name.clone(),
year: car.year,
rq: car.rq,
normalized_config: String::new(), // No config info available
track_count: 0, // Not best on any track
});
}
dropped_count += 1;
None
})
.collect();
if dropped_count > 0 {
eprintln!(
"Warning: {dropped_count} wishlist car(s) not found in best cars data and omitted from remove.csv"
"Warning: {dropped_count} wishlist car(s) not found in cars database and omitted from remove.csv"
);
}
@ -352,8 +383,24 @@ pub fn write_wishlist_files(
"remove.csv",
"RQ,Brand,Model,Year,FinalBuild,Count",
&to_remove,
|a, b| compare_records(&a.0, &b.0),
format_counted_row,
|a, b| {
a.rq.cmp(&b.rq)
.then(a.normalized_config.cmp(&b.normalized_config))
.then(a.brand.cmp(&b.brand))
.then(a.only_name.cmp(&b.only_name))
},
|entry, file| {
writeln!(
file,
"{},{},{},{},{},{}",
entry.rq,
escape_csv_field(&entry.brand),
escape_csv_field(&entry.only_name),
entry.year,
entry.normalized_config,
entry.track_count
)
},
)?;
Ok(())
@ -894,4 +941,55 @@ mod tests {
assert_eq!(count, 0, "Car already at final build should not appear in improve.csv");
}
// =======================================================================
// remove.csv tests for cars not in best cars data
// =======================================================================
#[test]
fn test_wishlist_removes_cars_not_in_best_data() {
// This tests the fix: cars on the wishlist that are NOT best on any track
// should still be included in remove.csv (using car_map as fallback)
let ctx = AnalysisContext::new();
// Only Car_A is best on tracks
let rec_a = make_record("Car_A", 85, 100.0, "guid_a");
for i in 0..15 {
ctx.best_cars.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_a.clone()]);
}
// Car_B and Car_C are NOT best on any track (they only exist in the car list)
let car_list = vec![
make_car("Car_A", 85, "guid_a"), // best on tracks
make_car("Car_B", 90, "guid_b"), // NOT best on any track
make_car("Car_C", 75, "guid_c"), // NOT best on any track
];
let counts = compute_car_counts(&ctx, car_list, empty_my_data());
let dir = tempfile::tempdir().unwrap();
let output_dir = dir.path().to_str().unwrap();
// Wishlist contains: guid_a (best car - should stay), guid_b (not best - should be removed), guid_c (not best - should be removed)
let wishlist: HashSet<String> = vec![
"guid_a".to_string(),
"guid_b".to_string(),
"guid_c".to_string(),
]
.into_iter()
.collect();
write_wishlist_files(output_dir, &counts, &HashSet::new(), &wishlist, 10, 300).unwrap();
// Check remove.csv exists and contains both cars not in best data
let remove_path = dir.path().join("remove.csv");
let content = fs::read_to_string(&remove_path).unwrap();
let lines: Vec<&str> = content.lines().collect();
// Should have header + 2 cars (guid_b and guid_c)
assert_eq!(lines.len(), 3, "remove.csv should have header + 2 cars, got: {:?}", lines);
// The cars should be in the file, sorted by RQ (75 first, then 90)
assert!(lines[1].starts_with("75,"), "First car should be RQ 75, got: {}", lines[1]);
assert!(lines[2].starts_with("90,"), "Second car should be RQ 90, got: {}", lines[2]);
}
}