pub fn neighbor_binding(base: &str, dx: i32, dy: i32) -> String {
if dx == 0 && dy == 0 {
base.to_string()
} else {
format!("{base}@{dx},{dy}")
}
}
pub fn neighbor_bindings(base: &str) -> Vec<String> {
let mut out = Vec::with_capacity(8);
for dy in -1..=1 {
for dx in -1..=1 {
if dx == 0 && dy == 0 {
continue;
}
out.push(neighbor_binding(base, dx, dy));
}
}
out
}
pub fn parse_neighbor_binding(name: &str) -> (&str, i32, i32) {
let Some((base, off)) = name.rsplit_once('@') else {
return (name, 0, 0);
};
let Some((sx, sy)) = off.split_once(',') else {
return (name, 0, 0);
};
match (sx.parse::<i32>(), sy.parse::<i32>()) {
(Ok(dx), Ok(dy)) if (-1..=1).contains(&dx) && (-1..=1).contains(&dy) => (base, dx, dy),
_ => (name, 0, 0),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
assert_eq!(neighbor_binding("s.l", 0, 0), "s.l");
assert_eq!(neighbor_binding("s.l", 1, 0), "s.l@1,0");
assert_eq!(neighbor_binding("s.l", -1, 1), "s.l@-1,1");
for dy in -1..=1 {
for dx in -1..=1 {
let name = neighbor_binding("roads.road", dx, dy);
assert_eq!(parse_neighbor_binding(&name), ("roads.road", dx, dy));
}
}
}
#[test]
fn eight_neighbors() {
let names = neighbor_bindings("s.l");
assert_eq!(names.len(), 8);
assert!(!names.iter().any(|n| n.ends_with("@0,0")));
assert_eq!(names[0], "s.l@-1,-1");
assert_eq!(names[7], "s.l@1,1");
}
#[test]
fn plain_and_malformed_pass_through() {
assert_eq!(parse_neighbor_binding("s.l"), ("s.l", 0, 0));
assert_eq!(parse_neighbor_binding("s.l@2,0"), ("s.l@2,0", 0, 0));
assert_eq!(parse_neighbor_binding("s.l@x,y"), ("s.l@x,y", 0, 0));
assert_eq!(
parse_neighbor_binding("http://h/{z}"),
("http://h/{z}", 0, 0)
);
}
}