From 3a72a667c557e84527be32e5fa7ff34953716868 Mon Sep 17 00:00:00 2001 From: Thomas Rasch Date: Mon, 8 Apr 2024 13:34:38 +0200 Subject: [PATCH] #45: Switch from Int ids to UInt ids if overflow happens and the id is not negative (#46) --- README.md | 4 ++ .../FeatureIdentifierExtensions.swift | 23 +++++++ Sources/GISTools/GeoJson/Feature.swift | 61 ++++++++++++++----- .../GISTools/GeoJson/GeoJsonConvertible.swift | 13 ++++ .../GISToolsTests/GeoJson/FeatureTests.swift | 25 ++++++++ 5 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 Sources/GISTools/Extensions/FeatureIdentifierExtensions.swift diff --git a/README.md b/README.md index b913761..c010ffc 100644 --- a/README.md +++ b/README.md @@ -603,9 +603,13 @@ func projected(to newProjection: Projection) -> GeometryCollection A `Feature` is sort of a container for exactly one GeoJSON geometry (`Point`, `MultiPoint`, `LineString`, `MultiLineString`, `Polygon`, `MultiPolygon`, `GeometryCollection`) together with some `properties` and an optional `id`: ```swift /// A GeoJSON identifier that can either be a string or number. +/// Any parsed integer value `Int64.min ⪬ i ⪬ Int64.max` will be cast to `Int` +/// (or `Int64` on 32-bit platforms), values above `Int64.max` will be cast to `UInt` +/// (or `UInt64` on 32-bit platforms). enum Identifier: Equatable, Hashable, CustomStringConvertible { case string(String) case int(Int) + case uint(UInt) case double(Double) } diff --git a/Sources/GISTools/Extensions/FeatureIdentifierExtensions.swift b/Sources/GISTools/Extensions/FeatureIdentifierExtensions.swift new file mode 100644 index 0000000..4bf5a14 --- /dev/null +++ b/Sources/GISTools/Extensions/FeatureIdentifierExtensions.swift @@ -0,0 +1,23 @@ +import Foundation + +// MARK: Public + +extension Feature.Identifier { + + public var int64Value: Int64? { + switch self { + case .int(let int): Int64(exactly: int) + case .uint(let uint): Int64(exactly: uint) + default: nil + } + } + + public var uint64Value: UInt64? { + switch self { + case .int(let int): UInt64(exactly: int) + case .uint(let uint): UInt64(exactly: uint) + default: nil + } + } + +} diff --git a/Sources/GISTools/GeoJson/Feature.swift b/Sources/GISTools/GeoJson/Feature.swift index f9f1f5c..97ab54e 100644 --- a/Sources/GISTools/GeoJson/Feature.swift +++ b/Sources/GISTools/GeoJson/Feature.swift @@ -4,40 +4,73 @@ import Foundation public struct Feature: GeoJson { /// A GeoJSON identifier that can either be a string or number. + /// + /// Any parsed integer value `Int64.min ⪬ i ⪬ Int64.max` will be cast to `Int` + /// (or `Int64` on 32-bit platforms), values above `Int64.max` will be cast to `UInt` + /// (or `UInt64` on 32-bit platforms). public enum Identifier: Equatable, Hashable, CustomStringConvertible, Sendable { + +#if _pointerBitWidth(_32) + public typealias IntId = Int64 + public typealias UIntId = UInt64 +#else + public typealias IntId = Int + public typealias UIntId = UInt +#endif + case string(String) - case int(Int) + case int(IntId) + case uint(UIntId) case double(Double) + /// Note: This will prefer `Int` over `UInt` if possible. public init?(value: Any?) { guard let value else { return nil } - if let int = value as? Int { + + switch value { + case let binaryInt as (any BinaryInteger): + if let int = IntId(exactly: binaryInt) { + self = .int(int) + } + else if let uint = UIntId(exactly: binaryInt) { + self = .uint(uint) + } + else { + return nil + } + + case let int as IntId: self = .int(int) - } - else if let string = value as? String { + + case let uint as UIntId: + self = .uint(uint) + + case let string as String: self = .string(string) - } - else if let double = value as? Double { + + case let double as Double: self = .double(double) - } - else { + + default: return nil } } public var asJson: Any { switch self { - case .double(let double): return double - case .int(let int): return int - case .string(let string): return string + case .double(let double): double + case .int(let int): int + case .uint(let uint): uint + case .string(let string): string } } public var description: String { switch self { - case .double(let double): return String(double) - case .int(let int): return String(int) - case .string(let string): return string + case .double(let double): String(double) + case .int(let int): String(int) + case .uint(let uint): String(uint) + case .string(let string): string } } } diff --git a/Sources/GISTools/GeoJson/GeoJsonConvertible.swift b/Sources/GISTools/GeoJson/GeoJsonConvertible.swift index 72a8db2..baa468e 100644 --- a/Sources/GISTools/GeoJson/GeoJsonConvertible.swift +++ b/Sources/GISTools/GeoJson/GeoJsonConvertible.swift @@ -103,3 +103,16 @@ extension Sequence where Self.Iterator.Element: GeoJsonWritable { } } + +// MARK: - Debugging + +extension GeoJsonWritable { + + /// Prints the receiver to the console. + public func dump() { + guard let stringified = asJsonString(prettyPrinted: true) else { return } + + print(stringified, separator: "") + } + +} diff --git a/Tests/GISToolsTests/GeoJson/FeatureTests.swift b/Tests/GISToolsTests/GeoJson/FeatureTests.swift index a585fc4..7f51af6 100644 --- a/Tests/GISToolsTests/GeoJson/FeatureTests.swift +++ b/Tests/GISToolsTests/GeoJson/FeatureTests.swift @@ -105,4 +105,29 @@ final class FeatureTests: XCTestCase { XCTAssertEqual(featureData, feature.asJsonData(prettyPrinted: true)) } + func testFeatureIds() throws { + XCTAssertEqual(Feature.Identifier(value: 1234), .int(1234)) + XCTAssertEqual(Feature.Identifier(value: Int8(32)), .int(32)) + XCTAssertEqual(Feature.Identifier(value: Int8(32))?.int64Value, 32) + XCTAssertEqual(Feature.Identifier(value: Int8(32))?.uint64Value, 32) + + XCTAssertEqual(Feature.Identifier(value: -1234), .int(-1234)) + XCTAssertEqual(Feature.Identifier(value: Int8(-32)), .int(-32)) + XCTAssertEqual(Feature.Identifier(value: Int8(-32))?.int64Value, -32) + XCTAssertNil(Feature.Identifier(value: Int8(-32))?.uint64Value) + + // UInt -> Int + XCTAssertEqual(Feature.Identifier(value: UInt64(32)), .int(32)) + + XCTAssertEqual(Feature.Identifier(value: Int64.max), .int(9223372036854775807)) + XCTAssertEqual(Feature.Identifier(value: Int64.max)?.int64Value, 9223372036854775807) + XCTAssertEqual(Feature.Identifier(value: Int64.min), .int(-9223372036854775808)) + XCTAssertEqual(Feature.Identifier(value: Int64.min)?.int64Value, -9223372036854775808) + + // 9223372036854775808 is Int64.max+1 + XCTAssertEqual(Feature.Identifier(value: UInt64(9223372036854775808)), .uint(9223372036854775808)) + XCTAssertNil(Feature.Identifier(value: UInt64(9223372036854775808))?.int64Value) + XCTAssertEqual(Feature.Identifier(value: UInt64(9223372036854775808))?.uint64Value, 9223372036854775808) + } + }