@Dennis89: In deiner Python-Lösung hast du zwei Mal `more_itertools.transpose` (unterschiedlich!) per Hand nachgebaut.
Die Rust-Lösung hat ein paar Stellen, an der man das besser machen kann und ein paar Stellen, an denen ich das anders machen würde (ohne dass ich da jetzt unbedingt sagen würde, dass das
besser ist.
Code: Alles auswählen
use std::error::Error;
use std::fs::File;
use std::io;
use std::io::BufRead;
use std::io::BufReader;
use std::iter::zip;
use std::path::Path;
use itertools::Itertools as _;
fn read_input(filepath: impl AsRef<Path>) -> io::Result<Vec<String>> {
let file = File::open(filepath)?;
let file = BufReader::new(file);
file.lines().collect()
}
fn parse_input(lines: Vec<String>) -> Result<(Vec<u64>, Vec<u64>), Box<dyn Error>> {
let (mut left_values, mut right_values): (Vec<u64>, Vec<u64>) = lines
.iter()
.map(|line| -> Result<(u64, u64), Box<dyn Error>> {
let mut parts = line.split_whitespace();
match (parts.next(), parts.next(), parts.next()) {
(Some(left), Some(right), None) => Ok((left.parse()?, right.parse()?)),
_ => Err("line did not contain two numbers".into()),
}
})
.process_results(|x| x.unzip())?;
left_values.sort();
right_values.sort();
Ok((left_values, right_values))
}
fn part_1(left_values: &[u64], right_values: &[u64]) -> u64 {
zip(left_values, right_values)
.map(|(left, &right)| left.abs_diff(right))
.sum()
}
fn part_2(left_values: &[u64], right_values: &[u64]) -> u64 {
let counts = right_values.iter().copied().counts();
left_values
.iter()
.map(|left| left * u64::try_from(*counts.get(left).unwrap_or(&0)).unwrap())
.sum()
}
fn main() -> Result<(), Box<dyn Error>> {
let lines = read_input("puzzel_input.txt")?;
let (left_values, right_values) = parse_input(lines)?;
println!("{}", part_1(&left_values, &right_values));
println!("{}", part_2(&left_values, &right_values));
Ok(())
}
In `read_lines` würde ich den Fragezeichenoperator benutzen, um „richtige“ Fehlerbehandlung zu machen und nicht das Programm bei einem Fehler abzuschießen. Damit spart man sich dann gleich das `map`, weil man einen `Iterator<Item = Result<T, E>>` direkt in ein `Result<Vec<T>, E>` collecten kann (das `collect` flippt das `Result` von innen nach außen).
`format_input` hat einen komischen Namen, eigentlich wird da der Input geparst, nicht formatiert.
Statt da einen `Vec<Vec<String>>` aufzubauen (das kopiert alles und allokiert mehrfach für jede Zeile; Allokationen will man in Rust normalerweise vermeiden, wenn es den Code nicht
viel komplizierter macht), kann man direkt vom `Vec<String>` in das Tupel `(Vec<u64>, Vec<u64>)` gehen, indem man `Itertools::process_results` benutzt. (Wenn man da `unwrap`/`panic!()` statt richtiger Fehlerbehandlung benutzt, kann man direkt `unzip` benutzen:
Code: Alles auswählen
let (mut left_values, mut right_values): (Vec<_>, Vec<_>) = lines
.iter()
.map(|line| {
let mut parts = line.split_whitespace();
match (parts.next(), parts.next(), parts.next()) {
(Some(left), Some(right), None) =>
(left.parse::<u64>().unwrap(), right.parse::<u64>().unwrap()),
_ => panic!("line did not contain two numbers: {line:?}"),
}
})
.unzip();
Du hast `part_1` und `part_2` so geschrieben, dass sie Ownership über die beiden `Vec`s übernehmen. Das ist in diesem Fall allerdings nicht nötig, weil du nur lesend auf die `Vec`s zugreifst. Also sollte man da Slices übergeben: `&[u64]`. Damit spart man sich dann auch das unnötige `clone()` in `main`.
In beiden Funktionen erstellst du einen Iterator (in der `for`-Schleife), pushst dann einen Haufen Werte in einen `Vec` und dann erstellst du aus dem `Vec` jeweils wieder einen Iterator, nur um darauf `sum` aufzurufen. Den `Vec` als Zwischenschritt brauchst du nicht, du kannst direkt `map` auf dem Ursprungsiterator benutzen und den dann mit `sum` konsumieren. (Das Python-Äquivalent dazu wäre, wenn du `sum([abs(l - r) for l, r in zip(ls, rs)])` statt `sum(abs(l - r) for l, r in zip(ls, rs))` geschrieben hättest.)
Ich habe den Code auch so umgebaut, dass überall `u64` benutzt wird. Die Werte können nie negativ werden, also ist `u64` ein guter Default für „damit will ich rechnen“.
In `part_2` benutzt du `as` zum Konvertieren vom `usize` aus `counts` nach `i32`. Ich persönlich würde von `as`
immer abraten, weil das zu viel kann. Wenn man irgendwann mal die Typen ändert, ist es möglich, dass das `as` nicht mehr macht, was man eigentlich mal wollte, aber man bekommt keinen Fehler, sondern einfach silently geändertes Verhalten.
Wenn man von einem kleineren in einen größeren Typen konvertiert, sollte man `Large::from()` oder `Small::into()` benutzen (das stellt sicher, dass die Konversion den Wert nie ändern kann). Wenn sich der Wert ändern kann, würde ich `try_from` bzw. `try_into` benutzen, damit man einen Fehler bekommt, wenn sich der Wert ändern würde. Hier auch gerne mit `unwrap`, weil alle `usize` immer auch in `u64` passen, wenn man „normale“ Hardware benutzt.