diff --git a/Cargo.lock b/Cargo.lock index f4a89d1..dfffb41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,8 @@ dependencies = [ "itertools", "lazy_static", "rstest", + "serde", + "serde_json", "unique_id", ] @@ -176,18 +178,19 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "indexmap" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown", + "serde", ] [[package]] @@ -199,6 +202,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "lazy_static" version = "1.5.0" @@ -293,9 +302,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rstest" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936" +checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" dependencies = [ "futures", "futures-timer", @@ -305,9 +314,9 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42" +checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" dependencies = [ "cfg-if", "glob", @@ -330,12 +339,50 @@ dependencies = [ "semver", ] +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +[[package]] +name = "serde" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "slab" version = "0.4.9" @@ -347,9 +394,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index ad79c9e..51660ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,10 @@ edition = "2021" [dependencies] -indexmap = "*" +indexmap = {version = "2.6.0", features = ["serde"]} itertools = "*" lazy_static = "1.5.0" -rstest = "*" +rstest = "0.23.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" unique_id = "*" \ No newline at end of file diff --git a/resources/test/polygon_1.json b/resources/test/polygon_1.json new file mode 100644 index 0000000..47cfd33 --- /dev/null +++ b/resources/test/polygon_1.json @@ -0,0 +1,10 @@ +{ + "vertices": { + "a": {"x": 0, "y": 0, "id": "a"}, + "b": {"x": 3, "y": 4, "id": "b"}, + "c": {"x": 6, "y": 2, "id": "c"}, + "d": {"x": 7, "y": 6, "id": "d"}, + "e": {"x": 3, "y": 9, "id": "e"}, + "f": {"x": -2, "y": 7, "id": "f"} + } +} \ No newline at end of file diff --git a/resources/test/polygon_2.json b/resources/test/polygon_2.json new file mode 100644 index 0000000..f6db082 --- /dev/null +++ b/resources/test/polygon_2.json @@ -0,0 +1,22 @@ +{ + "vertices": { + "0": {"x": 0, "y": 0, "id": "0"}, + "1": {"x": 12, "y": 9, "id": "1"}, + "2": {"x": 14, "y": 4, "id": "2"}, + "3": {"x": 24, "y": 10, "id": "3"}, + "4": {"x": 14, "y": 24, "id": "4"}, + "5": {"x": 11, "y": 17, "id": "5"}, + "6": {"x": 13, "y": 19, "id": "6"}, + "7": {"x": 14, "y": 12, "id": "7"}, + "8": {"x": 9, "y": 13, "id": "8"}, + "9": {"x": 7, "y": 18, "id": "9"}, + "10": {"x": 11, "y": 21, "id": "10"}, + "11": {"x": 8, "y": 24, "id": "11"}, + "12": {"x": 1, "y": 22, "id": "12"}, + "13": {"x": 2, "y": 18, "id": "13"}, + "14": {"x": 4, "y": 20, "id": "14"}, + "15": {"x": 6, "y": 10, "id": "15"}, + "16": {"x": -2, "y": 11, "id": "16"}, + "17": {"x": 6, "y": 6, "id": "17"} + } +} \ No newline at end of file diff --git a/resources/test/right_triangle.json b/resources/test/right_triangle.json new file mode 100644 index 0000000..aaa52d4 --- /dev/null +++ b/resources/test/right_triangle.json @@ -0,0 +1,7 @@ +{ + "vertices": { + "a": {"x": 0, "y": 0, "id": "a"}, + "b": {"x": 3, "y": 0, "id": "b"}, + "c": {"x": 0, "y": 4, "id": "c"} + } +} \ No newline at end of file diff --git a/resources/test/square_4x4.json b/resources/test/square_4x4.json new file mode 100644 index 0000000..5cd2588 --- /dev/null +++ b/resources/test/square_4x4.json @@ -0,0 +1,8 @@ +{ + "vertices": { + "a": {"x": 0, "y": 0, "id": "a"}, + "b": {"x": 4, "y": 0, "id": "b"}, + "c": {"x": 4, "y": 4, "id": "c"}, + "d": {"x": 0, "y": 4, "id": "d"} + } +} \ No newline at end of file diff --git a/src/polygon.rs b/src/polygon.rs index 1623c11..623e4aa 100644 --- a/src/polygon.rs +++ b/src/polygon.rs @@ -45,6 +45,27 @@ impl<'a> Polygon<'a> { pub fn from_vmap(vmap: &'a VertexMap) -> Polygon<'a> { Polygon::new(vmap.all_vertices()) } + + pub fn vertices(&self) -> Vec<&Vertex> { + // TODO can rethink edges method if this works + + let mut vertices = Vec::new(); + let mut current = self.anchor; + + loop { + vertices.push(current); + current = self.neighbors + .get(current) + .expect("Every vertex should have neighbors stored") + .1; + + if current == self.anchor { + break; + } + } + + vertices + } pub fn double_area(&self) -> i32 { // The first edge will include the anchor, but that area @@ -174,71 +195,60 @@ impl<'a> Polygon<'a> { mod tests { use super::*; use rstest::{fixture, rstest}; - use std::str::FromStr; + use std::path::PathBuf; - // TODO I think it will be better to ultimately read these - // from file since I'll likely have some with many vertices - // which will get a little unwieldy here. + fn load_vmap(filename: &str) -> VertexMap { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("resources/test"); + path.push(filename); + VertexMap::from_json(path) + } + + // TODO these fixtures are nice and compact now, but it has me + // wondering if this is the best way to support fixtures? Might + // get tedious specifying these as the test cases grow. Could + // just parameterize on filenames and then load vmap in the + // the test #[fixture] - fn polygon_1() -> &'static str { - concat!("0 0 a\n", "3 4 b\n", "6 2 c\n", "7 6 d\n", "3 9 e\n", "-2 7 f") + fn polygon_1() -> VertexMap { + load_vmap("polygon_1.json") } #[fixture] - fn polygon_2() -> &'static str { - concat!( - "0 0 0\n", - "12 9 1\n", - "14 4 2\n", - "24 10 3\n", - "14 24 4\n", - "11 17 5\n", - "13 19 6\n", - "14 12 7\n", - "9 13 8\n", - "7 18 9\n", - "11 21 10\n", - "8 24 11\n", - "1 22 12\n", - "2 18 13\n", - "4 20 14\n", - "6 10 15\n", - "-2 11 16\n", - "6 6 17" - ) + fn polygon_2() -> VertexMap { + load_vmap("polygon_2.json") } #[fixture] - fn right_triangle() -> &'static str { - concat!("0 0 a\n", "3 0 b\n", "0 4 c") + fn right_triangle() -> VertexMap { + load_vmap("right_triangle.json") } #[fixture] - fn square_4x4() -> &'static str { - concat!("0 0 a\n", "4 0 b\n", "4 4 c\n", "0 4 d") + fn square_4x4() -> VertexMap { + load_vmap("square_4x4.json") } - + + #[rstest] // TODO now that this is parametrized, can add as many polygons // here as possible to get meaningful tests on area #[case(right_triangle(), 12)] #[case(polygon_2(), 466)] - fn test_area(#[case] polygon_str: &str, #[case] expected_double_area: i32) { - let vmap = VertexMap::from_str(polygon_str).unwrap(); + fn test_area(#[case] vmap: VertexMap, #[case] expected_double_area: i32) { let polygon = Polygon::from_vmap(&vmap); let double_area = polygon.double_area(); assert_eq!(double_area, expected_double_area); } #[rstest] - fn test_neighbors_square(square_4x4: &str) { - let vmap = VertexMap::from_str(square_4x4).unwrap(); - let polygon = Polygon::from_vmap(&vmap); + fn test_neighbors_square(square_4x4: VertexMap) { + let polygon = Polygon::from_vmap(&square_4x4); - let a = vmap.get("a").unwrap(); - let b = vmap.get("b").unwrap(); - let c = vmap.get("c").unwrap(); - let d = vmap.get("d").unwrap(); + let a = square_4x4.get("a").unwrap(); + let b = square_4x4.get("b").unwrap(); + let c = square_4x4.get("c").unwrap(); + let d = square_4x4.get("d").unwrap(); assert_eq!(polygon.neighbors(a), (d, b)); assert_eq!(polygon.neighbors(b), (a, c)); @@ -247,16 +257,43 @@ mod tests { } #[rstest] - fn test_neighbors_p1(polygon_1: &str) { - let vmap = VertexMap::from_str(polygon_1).unwrap(); - let polygon = Polygon::from_vmap(&vmap); + fn test_edges_square(square_4x4: VertexMap) { + let polygon = Polygon::from_vmap(&square_4x4); + + let expected_edges = vec![ + square_4x4.get_line_segment("a", "b"), + square_4x4.get_line_segment("b", "c"), + square_4x4.get_line_segment("c", "d"), + square_4x4.get_line_segment("d", "a"), + ]; + + assert_eq!(polygon.edges(), expected_edges); + } + + #[rstest] + fn test_vertices_square(square_4x4: VertexMap) { + let polygon = Polygon::from_vmap(&square_4x4); + + let expected_vertices = vec![ + square_4x4.get("a").unwrap(), + square_4x4.get("b").unwrap(), + square_4x4.get("c").unwrap(), + square_4x4.get("d").unwrap(), + ]; + + assert_eq!(polygon.vertices(), expected_vertices); + } - let a = vmap.get("a").unwrap(); - let b = vmap.get("b").unwrap(); - let c = vmap.get("c").unwrap(); - let d = vmap.get("d").unwrap(); - let e = vmap.get("e").unwrap(); - let f = vmap.get("f").unwrap(); + #[rstest] + fn test_neighbors_p1(polygon_1: VertexMap) { + let polygon = Polygon::from_vmap(&polygon_1); + + let a = polygon_1.get("a").unwrap(); + let b = polygon_1.get("b").unwrap(); + let c = polygon_1.get("c").unwrap(); + let d = polygon_1.get("d").unwrap(); + let e = polygon_1.get("e").unwrap(); + let f = polygon_1.get("f").unwrap(); assert_eq!(polygon.neighbors(a), (f, b)); assert_eq!(polygon.neighbors(b), (a, c)); @@ -267,28 +304,59 @@ mod tests { } #[rstest] - fn test_diagonal(polygon_1: &str) { - let vmap = VertexMap::from_str(polygon_1).unwrap(); - let polygon = Polygon::from_vmap(&vmap); + fn test_edges_p1(polygon_1: VertexMap) { + let polygon = Polygon::from_vmap(&polygon_1); + + let expected_edges = vec![ + polygon_1.get_line_segment("a", "b"), + polygon_1.get_line_segment("b", "c"), + polygon_1.get_line_segment("c", "d"), + polygon_1.get_line_segment("d", "e"), + polygon_1.get_line_segment("e", "f"), + polygon_1.get_line_segment("f", "a"), + ]; + + assert_eq!(polygon.edges(), expected_edges); + } + + #[rstest] + fn test_vertices_p1(polygon_1: VertexMap) { + let polygon = Polygon::from_vmap(&polygon_1); + + let expected_vertices = vec![ + polygon_1.get("a").unwrap(), + polygon_1.get("b").unwrap(), + polygon_1.get("c").unwrap(), + polygon_1.get("d").unwrap(), + polygon_1.get("e").unwrap(), + polygon_1.get("f").unwrap(), + ]; + + assert_eq!(polygon.vertices(), expected_vertices); + } - let ac = vmap.get_line_segment("a", "c"); - let ad = vmap.get_line_segment("a", "d"); - let ae = vmap.get_line_segment("a", "e"); - let bd = vmap.get_line_segment("b", "d"); - let be = vmap.get_line_segment("b", "e"); - let bf = vmap.get_line_segment("b", "f"); - let ca = vmap.get_line_segment("c", "a"); - let ce = vmap.get_line_segment("c", "e"); - let cf = vmap.get_line_segment("c", "f"); - let da = vmap.get_line_segment("d", "a"); - let db = vmap.get_line_segment("d", "b"); - let df = vmap.get_line_segment("d", "f"); - let ea = vmap.get_line_segment("e", "a"); - let eb = vmap.get_line_segment("e", "b"); - let ec = vmap.get_line_segment("e", "c"); - let fb = vmap.get_line_segment("f", "b"); - let fc = vmap.get_line_segment("f", "c"); - let fd = vmap.get_line_segment("f", "d"); + #[rstest] + fn test_diagonal(polygon_1: VertexMap) { + let polygon = Polygon::from_vmap(&polygon_1); + + let ac = polygon_1.get_line_segment("a", "c"); + let ad = polygon_1.get_line_segment("a", "d"); + let ae = polygon_1.get_line_segment("a", "e"); + let bd = polygon_1.get_line_segment("b", "d"); + let be = polygon_1.get_line_segment("b", "e"); + let bf = polygon_1.get_line_segment("b", "f"); + let ca = polygon_1.get_line_segment("c", "a"); + let ce = polygon_1.get_line_segment("c", "e"); + let cf = polygon_1.get_line_segment("c", "f"); + let da = polygon_1.get_line_segment("d", "a"); + let db = polygon_1.get_line_segment("d", "b"); + let df = polygon_1.get_line_segment("d", "f"); + let ea = polygon_1.get_line_segment("e", "a"); + let eb = polygon_1.get_line_segment("e", "b"); + let ec = polygon_1.get_line_segment("e", "c"); + let fb = polygon_1.get_line_segment("f", "b"); + let fc = polygon_1.get_line_segment("f", "c"); + let fd = polygon_1.get_line_segment("f", "d"); let internal = vec![&ae, &bd, &be, &bf, &ce, &db, &df, &ea, &eb, &ec, &fb, &fd]; let external = vec![&ac, &ca]; @@ -316,26 +384,25 @@ mod tests { } #[rstest] - fn test_triangulation(polygon_2: &str) { - let vmap = VertexMap::from_str(polygon_2).unwrap(); - let polygon = Polygon::from_vmap(&vmap); + fn test_triangulation(polygon_2: VertexMap) { + let polygon = Polygon::from_vmap(&polygon_2); let triangulation = polygon.triangulation(); - let ls_17_1 = vmap.get_line_segment("17", "1"); - let ls_1_3 = vmap.get_line_segment("1", "3"); - let ls_4_6 = vmap.get_line_segment("4", "6"); - let ls_4_7 = vmap.get_line_segment("4", "7"); - let ls_9_11 = vmap.get_line_segment("9", "11"); - let ls_12_14 = vmap.get_line_segment("12", "14"); - let ls_15_17 = vmap.get_line_segment("15", "17"); - let ls_15_1 = vmap.get_line_segment("15", "1"); - let ls_15_3 = vmap.get_line_segment("15", "3"); - let ls_3_7 = vmap.get_line_segment("3", "7"); - let ls_11_14 = vmap.get_line_segment("11", "14"); - let ls_15_7 = vmap.get_line_segment("15", "7"); - let ls_15_8 = vmap.get_line_segment("15", "8"); - let ls_15_9 = vmap.get_line_segment("15", "9"); - let ls_9_14 = vmap.get_line_segment("9", "14"); + let ls_17_1 = polygon_2.get_line_segment("17", "1"); + let ls_1_3 = polygon_2.get_line_segment("1", "3"); + let ls_4_6 = polygon_2.get_line_segment("4", "6"); + let ls_4_7 = polygon_2.get_line_segment("4", "7"); + let ls_9_11 = polygon_2.get_line_segment("9", "11"); + let ls_12_14 = polygon_2.get_line_segment("12", "14"); + let ls_15_17 = polygon_2.get_line_segment("15", "17"); + let ls_15_1 = polygon_2.get_line_segment("15", "1"); + let ls_15_3 = polygon_2.get_line_segment("15", "3"); + let ls_3_7 = polygon_2.get_line_segment("3", "7"); + let ls_11_14 = polygon_2.get_line_segment("11", "14"); + let ls_15_7 = polygon_2.get_line_segment("15", "7"); + let ls_15_8 = polygon_2.get_line_segment("15", "8"); + let ls_15_9 = polygon_2.get_line_segment("15", "9"); + let ls_9_14 = polygon_2.get_line_segment("9", "14"); let expected = vec![ ls_17_1, @@ -356,6 +423,5 @@ mod tests { ]; assert_eq!(expected, triangulation); - } } diff --git a/src/vertex.rs b/src/vertex.rs index ac48d19..2410ff5 100644 --- a/src/vertex.rs +++ b/src/vertex.rs @@ -1,3 +1,4 @@ +use serde::{Deserialize, Serialize}; use std::str::FromStr; use unique_id::Generator; use unique_id::string::StringGenerator; @@ -15,7 +16,7 @@ lazy_static!( // TODO make this type-generic? // TODO handle dimensionality of coordinates -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct Vertex { pub x: i32, pub y: i32, @@ -86,6 +87,7 @@ impl Vertex { #[cfg(test)] mod tests { use super::*; + use serde_json; #[test] fn test_id() { @@ -100,6 +102,14 @@ mod tests { assert_eq!(Vertex::from_str("1 2 a"), expected); } + #[test] + fn test_serialize() { + let v = Vertex::new_with_id(1, 2, String::from("a")); + let serialized = serde_json::to_string(&v).unwrap(); + assert_eq!(serialized, "{\"x\":1,\"y\":2,\"id\":\"a\"}"); + let deserialized: Vertex = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, v); + } // TODO might be nice to add custom macro for between asserts, // not sure how difficult it is to write macros at this stage diff --git a/src/vertex_map.rs b/src/vertex_map.rs index 23a250e..d770e0e 100644 --- a/src/vertex_map.rs +++ b/src/vertex_map.rs @@ -1,17 +1,18 @@ -// use std::collections::HashMap; use indexmap::IndexMap; +use std::fs; +use std::path::Path; use std::str::FromStr; +use serde::{Deserialize, Serialize}; use crate::line_segment::LineSegment; use crate::vertex::Vertex; -#[derive(Debug, PartialEq)] +#[derive(Debug, Deserialize, PartialEq, Serialize)] pub struct VertexMap { vertices: IndexMap, } - #[derive(Debug, PartialEq)] pub struct ParseVertexMapError; @@ -25,6 +26,13 @@ impl VertexMap { VertexMap { vertices: vertex_map } } + pub fn from_json>(path: P) -> VertexMap { + let polygon_str: String = fs::read_to_string(path) + .expect("file should exist and be parseable"); + // TODO don't unwrap + serde_json::from_str(&polygon_str).unwrap() + } + pub fn get(&self, id: &str) -> Option<&Vertex> { self.vertices.get(id) } @@ -78,4 +86,20 @@ mod tests { let vmap = VertexMap::from_str(input).unwrap(); assert_eq!(vmap, expected_vmap); } + + #[test] + fn test_serialization() { + let a = Vertex::new_with_id(1, 2, String::from("a")); + let b = Vertex::new_with_id(3, 4, String::from("b")); + let vertices = vec![a, b]; + let vmap = VertexMap::new(vertices); + + let expected_serialized = r#"{"vertices":{"a":{"x":1,"y":2,"id":"a"},"b":{"x":3,"y":4,"id":"b"}}}"#; + + let serialized = serde_json::to_string(&vmap).unwrap(); + let deserialized: VertexMap = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(serialized, expected_serialized); + assert_eq!(deserialized, vmap); + } }