diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 06fc27e..43c38ef 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -19,7 +19,7 @@ jobs: build: name: 'Build and Test' - runs-on: windows-2019 + runs-on: windows-2025 env: VSTEST_CONNECTION_TIMEOUT: 900 diff --git a/.github/workflows/continous-benchmark.yml b/.github/workflows/continous-benchmark.yml index 9b84a0e..3b455a8 100644 --- a/.github/workflows/continous-benchmark.yml +++ b/.github/workflows/continous-benchmark.yml @@ -6,7 +6,7 @@ on: jobs: benchmark: name: Continuous benchmarking - runs-on: windows-2019 + runs-on: windows-2025 defaults: run: @@ -43,4 +43,4 @@ jobs: # GitHub API token to make a commit comment github-token: ${{ secrets.GITHUB_TOKEN }} # Upload the updated cache file for the next job by actions/cache - # Run `github-action-benchmark` action \ No newline at end of file + # Run `github-action-benchmark` action diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0be7637..2778ea3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: deploy: name: 'Deploy to Nuget' if: github.event_name == 'release' - runs-on: windows-2019 + runs-on: windows-2025 steps: - name: Validate release version diff --git a/src/GeoJSON.Text.Test.Unit/Feature/FeatureTests.cs b/src/GeoJSON.Text.Test.Unit/Feature/FeatureTests.cs index 3df4536..1b29c99 100644 --- a/src/GeoJSON.Text.Test.Unit/Feature/FeatureTests.cs +++ b/src/GeoJSON.Text.Test.Unit/Feature/FeatureTests.cs @@ -29,6 +29,25 @@ public void Can_Deserialize_Point_Feature() Assert.AreEqual(GeoJSONObjectType.Point, feature.Geometry.Type); } + [Test] + public void Can_Deserialize_Point_Feature_With_Numeric_Id() + { + var json = GetExpectedJson(); + + var feature = JsonSerializer.Deserialize(json); + + Assert.IsNotNull(feature); + Assert.IsNotNull(feature.Properties); + Assert.IsTrue(feature.Properties.Any()); + + Assert.IsTrue(feature.Properties.ContainsKey("name")); + Assert.AreEqual(feature.Properties["name"].ToString(), "Dinagat Islands"); + + Assert.AreEqual(17, feature.Id); + + Assert.AreEqual(GeoJSONObjectType.Point, feature.Geometry.Type); + } + [Test] public void Can_Serialize_LineString_Feature() { @@ -354,6 +373,32 @@ public void Serialized_And_Deserialized_Feature_Equals_And_Share_HashCode() Assert_Are_Equal(left, right); // assert id's + properties doesn't influence comparison and hashcode } + [Test] + public void Serialized_And_Deserialized_Feature_With_Numeric_Id_Equals_And_Share_HashCode() + { + var geometry = GetGeometry(); + + var leftFeature = new Text.Feature.Feature(geometry, null, 42); + var leftJson = JsonSerializer.Serialize(leftFeature); + var left = JsonSerializer.Deserialize(leftJson); + + var rightFeature = new Text.Feature.Feature(geometry, null, 42); + var rightJson = JsonSerializer.Serialize(rightFeature); + var right = JsonSerializer.Deserialize(rightJson); + + Assert_Are_Equal(left, right); // assert id's doesn't influence comparison and hashcode + + leftFeature = new Text.Feature.Feature(geometry, GetPropertiesInRandomOrder(), uint.MaxValue); + leftJson = JsonSerializer.Serialize(leftFeature); + left = JsonSerializer.Deserialize(leftJson); + + rightFeature = new Text.Feature.Feature(geometry, GetPropertiesInRandomOrder(), uint.MaxValue); + rightJson = JsonSerializer.Serialize(rightFeature); + right = JsonSerializer.Deserialize(rightJson); + + Assert_Are_Equal(left, right); // assert id's + properties doesn't influence comparison and hashcode + } + [Test] public void Feature_Equals_Null_Issue94() { @@ -541,4 +586,4 @@ private void Assert_Are_Not_Equal(Text.Feature.Feature left, Text.Feature.Featur Assert.AreNotEqual(left.GetHashCode(), right.GetHashCode()); } } -} \ No newline at end of file +} diff --git a/src/GeoJSON.Text.Test.Unit/Feature/FeatureTests_Can_Deserialize_Point_Feature_With_Numeric_Id.json b/src/GeoJSON.Text.Test.Unit/Feature/FeatureTests_Can_Deserialize_Point_Feature_With_Numeric_Id.json new file mode 100644 index 0000000..4d89a4b --- /dev/null +++ b/src/GeoJSON.Text.Test.Unit/Feature/FeatureTests_Can_Deserialize_Point_Feature_With_Numeric_Id.json @@ -0,0 +1,11 @@ +{ + "type": "Feature", + "id" : 17, + "geometry": { + "type": "Point", + "coordinates": [125.6, 10.1] + }, + "properties": { + "name": "Dinagat Islands" + } +} \ No newline at end of file diff --git a/src/GeoJSON.Text.Test.Unit/GeoJSON.Text.Test.Unit.csproj b/src/GeoJSON.Text.Test.Unit/GeoJSON.Text.Test.Unit.csproj index af09af1..f4c65f8 100644 --- a/src/GeoJSON.Text.Test.Unit/GeoJSON.Text.Test.Unit.csproj +++ b/src/GeoJSON.Text.Test.Unit/GeoJSON.Text.Test.Unit.csproj @@ -39,6 +39,7 @@ + diff --git a/src/GeoJSON.Text/Converters/FeatureIdConverter.cs b/src/GeoJSON.Text/Converters/FeatureIdConverter.cs new file mode 100644 index 0000000..effb8df --- /dev/null +++ b/src/GeoJSON.Text/Converters/FeatureIdConverter.cs @@ -0,0 +1,25 @@ +// Copyright © Joerg Battermann 2014, Matt Hunt 2017 + +using GeoJSON.Text.Feature; +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GeoJSON.Text.Converters +{ + internal class FeatureIdConverter : JsonConverter + { + public override FeatureId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.TokenType switch + { + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.TryGetUInt64(out var value) ? value : throw new JsonException(), + _ => throw new JsonException(), + }; + + public override void Write(Utf8JsonWriter writer, FeatureId value, JsonSerializerOptions options) + { + if (value.IsNumeric) writer.WriteNumberValue(value); + else writer.WriteStringValue(value); + } + } +} \ No newline at end of file diff --git a/src/GeoJSON.Text/Feature/Feature.cs b/src/GeoJSON.Text/Feature/Feature.cs index ba4d2bd..5b0590d 100644 --- a/src/GeoJSON.Text/Feature/Feature.cs +++ b/src/GeoJSON.Text/Feature/Feature.cs @@ -21,7 +21,7 @@ namespace GeoJSON.Text.Feature public class Feature : GeoJSONObject, IEquatable> where TGeometry : IGeometryObject { - private string _id; + private FeatureId _id; private bool _idHasValue = false; private TGeometry _geometry; private bool _geometryHasValue = false; @@ -33,14 +33,14 @@ public Feature() } - public Feature(TGeometry geometry, TProps properties, string id = null) + public Feature(TGeometry geometry, TProps properties, FeatureId id = null) { Geometry = geometry; Properties = properties; Id = id; } - public Feature(IGeometryObject geometry, TProps properties, string id = null) + public Feature(IGeometryObject geometry, TProps properties, FeatureId id = null) { Geometry = (TGeometry)geometry; Properties = properties; @@ -53,7 +53,8 @@ public Feature(IGeometryObject geometry, TProps properties, string id = null) [JsonPropertyName( "id")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Id { + [JsonConverter(typeof(FeatureIdConverter))] + public FeatureId Id { get { return _id; @@ -195,12 +196,12 @@ public Feature() } - public Feature(IGeometryObject geometry, IDictionary properties = null, string id = null) + public Feature(IGeometryObject geometry, IDictionary properties = null, FeatureId id = null) : base(geometry, properties, id) { } - public Feature(IGeometryObject geometry, object properties, string id = null) + public Feature(IGeometryObject geometry, object properties, FeatureId id = null) : base(geometry, properties, id) { } @@ -225,7 +226,7 @@ public Feature() /// The Geometry Object. /// The properties. /// The (optional) identifier. - public Feature(TGeometry geometry, IDictionary properties = null, string id = null) + public Feature(TGeometry geometry, IDictionary properties = null, FeatureId id = null) : base(geometry, properties ?? new Dictionary(), id) { } @@ -236,7 +237,7 @@ public Feature(TGeometry geometry, IDictionary properties = null /// The Geometry Object. /// The properties. /// The (optional) identifier. - public Feature(IGeometryObject geometry, IDictionary properties = null, string id = null) + public Feature(IGeometryObject geometry, IDictionary properties = null, FeatureId id = null) : base((TGeometry)geometry, properties ?? new Dictionary(), id) { } @@ -250,7 +251,7 @@ public Feature(IGeometryObject geometry, IDictionary properties /// properties /// /// The (optional) identifier. - public Feature(TGeometry geometry, object properties, string id = null) + public Feature(TGeometry geometry, object properties, FeatureId id = null) : this(geometry, GetDictionaryOfPublicProperties(properties), id) { } @@ -313,4 +314,44 @@ public override int GetHashCode() return !(left?.Equals(right) ?? right is null); } } + + public sealed class FeatureId : IEquatable, + IEquatable, + IEquatable, IEquatable, IEquatable, IEquatable, + IEquatable, IEquatable, IEquatable, IEquatable + { + private readonly string _strId; + private readonly ulong? _numId; + + private FeatureId(string str, ulong? num) => (_strId, _numId) = (str, num); + + public static implicit operator FeatureId(string str) => new(str, null); + public static implicit operator FeatureId(ulong num) => new(null, num); + + public bool IsNumeric => _numId.HasValue; + public bool IsString => !IsNumeric; + + public static implicit operator string(FeatureId id) => id.IsString ? id._strId : throw new InvalidCastException(); + public static implicit operator ulong(FeatureId id) => id.IsNumeric ? id._numId.Value : throw new InvalidCastException(); + + public override int GetHashCode() => (_strId.GetHashCode() * 397) ^ _numId.GetHashCode(); + + public bool Equals(FeatureId other) => _strId == other._strId && _numId == other._numId; + + public bool Equals(string other) => Equals((FeatureId)other); + public bool Equals(ulong other) => Equals((FeatureId)other); + public bool Equals(uint other) => Equals((FeatureId)other); + public bool Equals(ushort other) => Equals((FeatureId)other); + public bool Equals(byte other) => Equals((FeatureId)other); + public bool Equals(long other) => other >= 0 && Equals((ulong)other); + public bool Equals(int other) => other >= 0 && Equals((ulong)other); + public bool Equals(short other) => other >= 0 && Equals((ulong)other); + public bool Equals(sbyte other) => other >= 0 && Equals((ulong)other); + + public override bool Equals(object obj) => obj is FeatureId id && Equals(id); + + public static bool operator ==(FeatureId left, FeatureId right) => left.Equals(right); + + public static bool operator !=(FeatureId left, FeatureId right) => !(left == right); + } }