diff --git a/Sources/GISTools/Algorithms/EnumerateProperties.swift b/Sources/GISTools/Algorithms/EnumerateProperties.swift index 1020b3e..4746e8e 100644 --- a/Sources/GISTools/Algorithms/EnumerateProperties.swift +++ b/Sources/GISTools/Algorithms/EnumerateProperties.swift @@ -5,13 +5,34 @@ import Foundation extension FeatureCollection { - /// Enumerate all properties in the FeatureCollection with feature index.. + /// Enumerate over all Feature properties in the FeatureCollection. /// - /// - Parameter callback: The callback function + /// - Parameter callback: The callback function, called for each Feature with Feature index and properties public func enumerateProperties(_ callback: (_ featureIndex: Int, _ properties: [String: Sendable]) -> Void) { for (featureIndex, feature) in features.enumerated() { callback(featureIndex, feature.properties) } } + /// Creates a summary over all properties in the FeatureCollection. + /// + /// - Returns: A dictionary with all the keys found in all properties of the FeatureCollection, + /// and the values are the distinct values for each key. + /// + /// - Note: All valid JSON types are `Hashable`. + public func propertiesSummary() -> [String: [AnyHashable]] { + var keyValuePairs: [(String, Set)] = [] + + for feature in features { + for (key, value) in feature.properties { + if let hashable = value as? AnyHashable { + keyValuePairs.append((key, Set([hashable]))) + } + } + } + + return Dictionary(keyValuePairs, uniquingKeysWith: { $0.union($1) }) + .mapValues(\.asArray) + } + } diff --git a/Sources/GISTools/Extensions/SetExtensions.swift b/Sources/GISTools/Extensions/SetExtensions.swift new file mode 100644 index 0000000..ea5dc0c --- /dev/null +++ b/Sources/GISTools/Extensions/SetExtensions.swift @@ -0,0 +1,12 @@ +import Foundation + +// MARK: Private + +extension Set { + + /// Converts the Set to an Array + var asArray: [Element] { + Array(self) + } + +} diff --git a/Tests/GISToolsTests/GeoJson/FeatureCollectionTests.swift b/Tests/GISToolsTests/GeoJson/FeatureCollectionTests.swift index be7fd96..3493cf1 100644 --- a/Tests/GISToolsTests/GeoJson/FeatureCollectionTests.swift +++ b/Tests/GISToolsTests/GeoJson/FeatureCollectionTests.swift @@ -150,7 +150,7 @@ final class FeatureCollectionTests: XCTestCase { (2, 4, Coordinate3D(latitude: 0.0, longitude: 100.0)) ] - var result:[(Int, Int, Coordinate3D)] = [] + var result: [(Int, Int, Coordinate3D)] = [] featureCollection.enumerateCoordinates { featureIndex, coordinateIndex, coordinate in result.append((featureIndex, coordinateIndex, coordinate)) } @@ -164,6 +164,39 @@ final class FeatureCollectionTests: XCTestCase { } } + func testEnumerateProperties() throws { + let featureCollection = try XCTUnwrap(FeatureCollection(jsonString: FeatureCollectionTests.featureCollectionJson)) + + let expected: [(Int, [String: Sendable])] = [ + (0, ["prop0": "value0"]), + (1, ["prop0": "value0", "prop1": 0]), + (2, ["prop0": "value0", "prop1": ["this": "that"]]) + ] + + var result: [(Int, [String: Sendable])] = [] + featureCollection.enumerateProperties { featureIndex, properties in + XCTAssertFalse(properties.isEmpty) + result.append((featureIndex, properties)) + } + + XCTAssertEqual(result.count, expected.count) + + for (lhs, rhs) in zip(result, expected) { + XCTAssertEqual(lhs.0, rhs.0) + } + } + + func testPropertiesSummary() throws { + let featureCollection = try XCTUnwrap(FeatureCollection(jsonString: FeatureCollectionTests.featureCollectionJson)) + + let summary = featureCollection.propertiesSummary() + XCTAssertEqual(summary.count, 2) + XCTAssertEqual(summary.keys.sorted(), ["prop0", "prop1"]) + XCTAssertEqual(summary["prop0"], ["value0"]) + XCTAssertTrue(summary["prop1"]!.contains(0)) + XCTAssertTrue(summary["prop1"]!.contains(["this": "that"])) + } + func testEncodable() throws { let featureCollection = try XCTUnwrap(FeatureCollection(jsonString: FeatureCollectionTests.featureCollectionJson))