Compare commits

...

5 Commits
0.1.0 ... main

Author SHA1 Message Date
b498ec8616 added GetHash helper
All checks were successful
Publish NuGet Packages / build-and-publish (push) Successful in 25s
2025-10-08 11:38:29 -04:00
c6e6b29905 refactor Country to CountryCode to better reflect the value it contain
All checks were successful
Publish NuGet Packages / build-and-publish (push) Successful in 29s
2025-10-08 11:35:23 -04:00
854658f732 added Address CreateFrom helper
All checks were successful
Publish NuGet Packages / build-and-publish (push) Successful in 38s
2025-10-08 11:29:16 -04:00
075a803f4f fix tests with claude
All checks were successful
Publish NuGet Packages / build-and-publish (push) Successful in 27s
2025-10-08 11:11:14 -04:00
1c1b2877cf added From, To and switch GetFormattedAddress to IAddress extension 2025-10-08 10:58:12 -04:00
8 changed files with 170 additions and 107 deletions

View File

@ -2,10 +2,10 @@ namespace Svrnty.GeoManagement.Abstractions.Abstractions;
public interface IAddress
{
public string Line1 { get; }
public string? Line2 { get; }
public string City { get; }
public string Subdivision { get; }
public string PostalCode { get; }
public string Country { get; }
public string Line1 { get; set; }
public string? Line2 { get; set; }
public string City { get; set; }
public string Subdivision { get; set; }
public string PostalCode { get; set; }
public string CountryCode { get; set; }
}

View File

@ -2,60 +2,32 @@ using Svrnty.GeoManagement.Abstractions.Abstractions;
namespace Svrnty.GeoManagement.Abstractions.Models;
public record GeoPoint(double Latitude, double Longitude);
public record GeoPoint(decimal Latitude, decimal Longitude);
public record Address(
string Line1,
string? Line2,
string City,
string Subdivision,
string PostalCode,
string Country,
GeoPoint? Location,
string? Note,
bool IsNormalized = false
) : IAddress
public record Address(bool Normalized = false) : IAddress
{
public string GetFormattedAddress(
FormattedAddressType formatType = FormattedAddressType.Full
)
public required string Line1 { get; set; }
public string? Line2 { get; set; }
public required string City { get; set; }
public required string Subdivision { get; set; }
public required string PostalCode { get; set; }
public required string CountryCode { get; set; }
public GeoPoint? Location { get; set; }
public string? Note { get; set; }
public bool IsNormalized() => Normalized;
public static Address CreateFrom(IAddress address, GeoPoint? location = null, string? note = null, bool normalized = false)
{
return formatType switch
return new Address(normalized)
{
FormattedAddressType.Full => FormatFullOneLine(),
FormattedAddressType.Compact => FormatCompactOneLine(),
FormattedAddressType.MultiLine => FormatMultiLine(),
_ => FormatFullOneLine()
Line1 = address.Line1,
Line2 = address.Line2,
City = address.City,
Subdivision = address.Subdivision,
PostalCode = address.PostalCode,
CountryCode = address.CountryCode,
Location = location,
Note = note,
};
}
private string FormatFullOneLine()
{
if (string.IsNullOrWhiteSpace(Line2))
{
return $"{Line1}, {City}, {Subdivision} {PostalCode}, {Country}";
}
return $"{Line2}, {Line1}, {City}, {Subdivision} {PostalCode}, {Country}";
}
private string FormatCompactOneLine()
{
if (string.IsNullOrWhiteSpace(Line2))
{
return $"{Line1}, {City}, {Subdivision}";
}
return $"{Line2}, {Line1}, {City}, {Subdivision}";
}
private string FormatMultiLine()
{
if (string.IsNullOrWhiteSpace(Line2))
{
return $"{Line1}\n{City}, {Subdivision} {PostalCode}\n{Country}";
}
return $"{Line2}, {Line1}\n{City}, {Subdivision} {PostalCode}\n{Country}";
}
}

View File

@ -0,0 +1,82 @@
using System.Security.Cryptography;
using System.Text;
using Svrnty.GeoManagement.Abstractions.Abstractions;
using Svrnty.GeoManagement.Abstractions.Models;
namespace Svrnty.GeoManagement.Abstractions;
public static class ServiceCollectionExtensions
{
public static void To(this IAddress address, IAddress toAddress)
{
toAddress.Line1 = address.Line1;
toAddress.Line2 = address.Line2;
toAddress.City = address.City;
toAddress.PostalCode = address.PostalCode;
toAddress.CountryCode = address.CountryCode;
toAddress.Subdivision = address.Subdivision;
}
public static void From(this IAddress address, IAddress fromAddress)
{
address.Line1 = fromAddress.Line1;
address.Line2 = fromAddress.Line2;
address.City = fromAddress.City;
address.PostalCode = fromAddress.PostalCode;
address.CountryCode = fromAddress.CountryCode;
address.Subdivision = fromAddress.Subdivision;
}
public static string GetFormattedAddress(this IAddress address, FormattedAddressType formatType = FormattedAddressType.Full)
{
return formatType switch
{
FormattedAddressType.Full => FormatFullOneLine(address),
FormattedAddressType.Compact => FormatCompactOneLine(address),
FormattedAddressType.MultiLine => FormatMultiLine(address),
_ => FormatFullOneLine(address)
};
}
private static string FormatFullOneLine(IAddress address)
{
if (string.IsNullOrWhiteSpace(address.Line2))
{
return $"{address.Line1}, {address.City}, {address.Subdivision} {address.PostalCode}, {address.CountryCode}";
}
return $"{address.Line2}, {address.Line1}, {address.City}, {address.Subdivision} {address.PostalCode}, {address.CountryCode}";
}
private static string FormatCompactOneLine(IAddress address)
{
if (string.IsNullOrWhiteSpace(address.Line2))
{
return $"{address.Line1}, {address.City}, {address.Subdivision}";
}
return $"{address.Line2}, {address.Line1}, {address.City}, {address.Subdivision}";
}
private static string FormatMultiLine(IAddress address)
{
if (string.IsNullOrWhiteSpace(address.Line2))
{
return $"{address.Line1}\n{address.City}, {address.Subdivision} {address.PostalCode}\n{address.CountryCode}";
}
return $"{address.Line2}, {address.Line1}\n{address.City}, {address.Subdivision} {address.PostalCode}\n{address.CountryCode}";
}
public static string GetHash(this IAddress address)
=> ComputeSha256Hex(address.GetFormattedAddress());
private static string ComputeSha256Hex(string input)
{
using SHA256 sha256 = SHA256.Create();
byte[] bytes = Encoding.UTF8.GetBytes(input);
byte[] hash = sha256.ComputeHash(bytes);
return Convert.ToHexString(hash).ToLower();
}
}

View File

@ -3,6 +3,7 @@ using GoogleApi.Entities.Maps.Geocoding.Address.Request;
using GoogleApi.Entities.Maps.Geocoding.Location.Request;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Svrnty.GeoManagement.Abstractions;
using Svrnty.GeoManagement.Abstractions.Abstractions;
using Svrnty.GeoManagement.Abstractions.Models;
using Svrnty.GeoManagement.Google.Configuration;
@ -91,7 +92,7 @@ public class GeoManagementGoogleProvider : IGeoManagementProvider
var request = new LocationGeocodeRequest
{
Key = _options.ApiKey,
Location = new GoogleApi.Entities.Common.Coordinate(geoPoint.Latitude, geoPoint.Longitude)
Location = new GoogleApi.Entities.Common.Coordinate((double)geoPoint.Latitude, (double)geoPoint.Longitude)
};
if (!string.IsNullOrWhiteSpace(_options.Language) &&

View File

@ -26,20 +26,20 @@ internal static class GoogleAddressMapper
var country = GetComponentValue(components, "country") ?? string.Empty;
var location = googleResult.Geometry?.Location != null
? new GeoPoint(googleResult.Geometry.Location.Latitude, googleResult.Geometry.Location.Longitude)
? new GeoPoint((decimal)googleResult.Geometry.Location.Latitude, (decimal)googleResult.Geometry.Location.Longitude)
: null;
return new Abstractions.Models.Address(
Line1: line1,
Line2: line2,
City: city,
Subdivision: subdivision,
PostalCode: postalCode,
Country: country,
Location: location,
Note: null,
IsNormalized: true
);
return new Abstractions.Models.Address(Normalized: true)
{
Line1 = line1,
Line2 = line2,
City = city,
Subdivision = subdivision,
PostalCode = postalCode,
CountryCode = country,
Location = location,
Note = null
};
}
private static string BuildLine1(IEnumerable<AddressComponent> components)
@ -79,8 +79,8 @@ internal static class GoogleAddressMapper
return null;
return new GeoPoint(
googleResult.Geometry.Location.Latitude,
googleResult.Geometry.Location.Longitude
(decimal)googleResult.Geometry.Location.Latitude,
(decimal)googleResult.Geometry.Location.Longitude
);
}
}

View File

@ -24,12 +24,12 @@ public class AddressTestData
public class GeoPointTestData
{
public double Latitude { get; set; }
public double Longitude { get; set; }
public decimal Latitude { get; set; }
public decimal Longitude { get; set; }
}
public class CoordinateRange
{
public double Min { get; set; }
public double Max { get; set; }
public decimal Min { get; set; }
public decimal Max { get; set; }
}

View File

@ -54,15 +54,17 @@ public class GoogleProviderTests : IDisposable
public async Task GetGeoPointAsync_WithValidAddress_ReturnsCoordinates()
{
// Arrange
var address = new Address(
Line1: _testData.ValidAddress.Line1,
Line2: _testData.ValidAddress.Line2,
City: _testData.ValidAddress.City,
Subdivision: _testData.ValidAddress.Subdivision,
PostalCode: _testData.ValidAddress.PostalCode,
Country: _testData.ValidAddress.Country,
Location: null,
Note: null);
var address = new Address
{
Line1 = _testData.ValidAddress.Line1,
Line2 = _testData.ValidAddress.Line2,
City = _testData.ValidAddress.City,
Subdivision = _testData.ValidAddress.Subdivision,
PostalCode = _testData.ValidAddress.PostalCode,
CountryCode = _testData.ValidAddress.Country,
Location = null,
Note = null
};
// Act
var result = await _provider.GetGeoPointAsync(address);
@ -88,8 +90,8 @@ public class GoogleProviderTests : IDisposable
Assert.NotNull(result);
Assert.NotNull(result.Line1);
Assert.NotNull(result.City);
Assert.NotNull(result.Country);
Assert.True(result.IsNormalized);
Assert.NotNull(result.CountryCode);
Assert.True(result.IsNormalized());
Assert.NotNull(result.Location);
}
@ -97,22 +99,24 @@ public class GoogleProviderTests : IDisposable
public async Task NormalizeAddressAsync_WithAddressObject_ReturnsNormalizedAddress()
{
// Arrange
var address = new Address(
Line1: _testData.NormalizeTestAddress.Line1,
Line2: _testData.NormalizeTestAddress.Line2,
City: _testData.NormalizeTestAddress.City,
Subdivision: _testData.NormalizeTestAddress.Subdivision,
PostalCode: _testData.NormalizeTestAddress.PostalCode,
Country: _testData.NormalizeTestAddress.Country,
Location: null,
Note: null);
var address = new Address
{
Line1 = _testData.NormalizeTestAddress.Line1,
Line2 = _testData.NormalizeTestAddress.Line2,
City = _testData.NormalizeTestAddress.City,
Subdivision = _testData.NormalizeTestAddress.Subdivision,
PostalCode = _testData.NormalizeTestAddress.PostalCode,
CountryCode = _testData.NormalizeTestAddress.Country,
Location = null,
Note = null
};
// Act
var result = await _provider.NormalizeAddressAsync(address);
// Assert
Assert.NotNull(result);
Assert.True(result.IsNormalized);
Assert.True(result.IsNormalized());
Assert.NotNull(result.Location);
Assert.NotNull(result.Line1);
Assert.Contains("Amphitheatre", result.Line1, StringComparison.OrdinalIgnoreCase);
@ -129,7 +133,7 @@ public class GoogleProviderTests : IDisposable
// Assert
Assert.NotNull(result);
Assert.True(result.IsNormalized);
Assert.True(result.IsNormalized());
Assert.NotNull(result.Location);
Assert.NotNull(result.Line1);
Assert.Contains("Mountain View", result.City, StringComparison.OrdinalIgnoreCase);
@ -139,15 +143,17 @@ public class GoogleProviderTests : IDisposable
public async Task GetGeoPointAsync_WithInvalidAddress_ReturnsNull()
{
// Arrange
var address = new Address(
Line1: _testData.InvalidAddress.Line1,
Line2: _testData.InvalidAddress.Line2,
City: _testData.InvalidAddress.City,
Subdivision: _testData.InvalidAddress.Subdivision,
PostalCode: _testData.InvalidAddress.PostalCode,
Country: _testData.InvalidAddress.Country,
Location: null,
Note: null);
var address = new Address
{
Line1 = _testData.InvalidAddress.Line1,
Line2 = _testData.InvalidAddress.Line2,
City = _testData.InvalidAddress.City,
Subdivision = _testData.InvalidAddress.Subdivision,
PostalCode = _testData.InvalidAddress.PostalCode,
CountryCode = _testData.InvalidAddress.Country,
Location = null,
Note = null
};
// Act
var result = await _provider.GetGeoPointAsync(address);
@ -172,7 +178,7 @@ public class GoogleProviderTests : IDisposable
// This test just verifies it doesn't crash
if (result != null)
{
Assert.True(result.IsNormalized);
Assert.True(result.IsNormalized());
}
}

View File

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F728eee8ccad23b8fff516dba093eb71b49fb5ee8839f2118392e9747de4ebb_003FResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>