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:
parent
fc9f637c60
commit
4ecfc2d14d
1 changed files with 112 additions and 14 deletions
|
|
@ -320,30 +320,61 @@ pub fn write_wishlist_files(
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// --- remove.csv (wishlist cars not in the ideal set) ---
|
// --- 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 mut dropped_count = 0usize;
|
||||||
let to_remove: Vec<_> = wishlist
|
let to_remove: Vec<RemoveEntry> = wishlist
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|guid| !ideal_guids.contains(*guid))
|
.filter(|guid| !ideal_guids.contains(*guid))
|
||||||
.filter_map(|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) {
|
if let Some(keys) = counts.guid_to_rid.get(guid) {
|
||||||
// Pick the entry with the highest count (most impactful config)
|
// 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))
|
.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(|| {
|
if let Some((record, count)) = counts.by_rid.get(key) {
|
||||||
dropped_count += 1;
|
return Some(RemoveEntry {
|
||||||
None
|
brand: record.brand.clone(),
|
||||||
})
|
only_name: record.only_name.clone(),
|
||||||
} else {
|
year: record.year,
|
||||||
dropped_count += 1;
|
rq: record.rq,
|
||||||
None
|
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();
|
.collect();
|
||||||
if dropped_count > 0 {
|
if dropped_count > 0 {
|
||||||
eprintln!(
|
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",
|
"remove.csv",
|
||||||
"RQ,Brand,Model,Year,FinalBuild,Count",
|
"RQ,Brand,Model,Year,FinalBuild,Count",
|
||||||
&to_remove,
|
&to_remove,
|
||||||
|a, b| compare_records(&a.0, &b.0),
|
|a, b| {
|
||||||
format_counted_row,
|
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(())
|
Ok(())
|
||||||
|
|
@ -894,4 +941,55 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(count, 0, "Car already at final build should not appear in improve.csv");
|
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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue