Skip to content

Commit

Permalink
#45: Switch from Int ids to UInt ids if overflow happens and the id i…
Browse files Browse the repository at this point in the history
…s not negative (#46)
  • Loading branch information
trasch authored Apr 8, 2024
1 parent 30e1e1f commit 3a72a66
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 14 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
23 changes: 23 additions & 0 deletions Sources/GISTools/Extensions/FeatureIdentifierExtensions.swift
Original file line number Diff line number Diff line change
@@ -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
}
}

}
61 changes: 47 additions & 14 deletions Sources/GISTools/GeoJson/Feature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions Sources/GISTools/GeoJson/GeoJsonConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: "")
}

}
25 changes: 25 additions & 0 deletions Tests/GISToolsTests/GeoJson/FeatureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

}

0 comments on commit 3a72a66

Please sign in to comment.