Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug/319 localization double parse #349

Merged
merged 8 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 44 additions & 11 deletions src/dymaptic.GeoBlazor.Core/Objects/AttributesDictionary.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using dymaptic.GeoBlazor.Core.Components;
using ProtoBuf;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Reflection.Metadata;
using System.Text.Json;
Expand All @@ -16,21 +17,28 @@ namespace dymaptic.GeoBlazor.Core.Objects;
public class AttributesDictionary : IEquatable<AttributesDictionary>
{
/// <summary>
/// Constructor
/// Constructor for a new, empty dictionary
/// </summary>
public AttributesDictionary()
{
_backingDictionary = new Dictionary<string, object?>();
}

/// <summary>
/// Constructor from existing dictionary
/// Constructor from existing dictionary or JSON deserialized dictionary
/// </summary>
/// <param name="dictionary">
/// The dictionary to use
/// The dictionary to use.
/// </param>
/// <remarks>
/// It is possible to control the culture used for parsing numbers and dates by including a "geoBlazorCulture" key in the dictionary. The value should be a string representation of a culture name, such as "en-US" or "fr-FR".
/// Alternatively, for a server-generated dictionary, the culture can be set in the server's CultureInfo.DefaultThreadCurrentUICulture property. It will fall back to `CultureInfo.CurrentCulture` if not set.
/// </remarks>
public AttributesDictionary(Dictionary<string, object?> dictionary)
{
CultureInfo cultureInfo = dictionary.TryGetValue("geoBlazorCulture", out object? gbCulture)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to make this a property on the class rather then another entry in the dictionary? It could help separate data vs framework, unless these attributes are not directly realted to the data.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is the serialization of the attributes, where we have to switch them to an array of objects with key, value, and valueType. I could rewrite all of that to be an object with an array, but this seemed simpler. Rewriting would mean rewriting both JSON and Protobuf serialization methods in both C# and TypeScript. 😩

? new CultureInfo(gbCulture!.ToString()!)
: CultureInfo.DefaultThreadCurrentUICulture ?? CultureInfo.CurrentCulture;
_backingDictionary = new Dictionary<string, object?>();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };

Expand All @@ -44,19 +52,23 @@ public AttributesDictionary(Dictionary<string, object?> dictionary)
JsonValueKind.Array => jsonElement.Deserialize(typeof(IEnumerable<object>), options),
JsonValueKind.False => false,
JsonValueKind.True => true,
JsonValueKind.Number => jsonElement.ToString().Contains('.')
? Convert.ChangeType(jsonElement.ToString(), TypeCode.Double)
: Convert.ChangeType(jsonElement.ToString(), TypeCode.Int64),
JsonValueKind.Number => double.Parse(jsonElement.ToString(), cultureInfo),
JsonValueKind.String => jsonElement.ToString(),
_ => jsonElement
};
if (typedValue is string stringValue && Guid.TryParse(stringValue, out Guid guidValue))
if (typedValue is string stringValue)
{
typedValue = guidValue;
if (Guid.TryParse(stringValue, out Guid guidValue))
{
typedValue = guidValue;
}
else if (DateTime.TryParse(stringValue, cultureInfo, DateTimeStyles.None, out DateTime dateValue))
{
typedValue = dateValue;
}
}
_backingDictionary[kvp.Key] = (typedValue ?? default(object?))!;
}

// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
else if (kvp.Value is null) // could be null from serialization
{
Expand All @@ -69,18 +81,39 @@ public AttributesDictionary(Dictionary<string, object?> dictionary)
}
}

/// <summary>
/// Internal constructor for use with Protobuf deserialization
/// </summary>
/// <param name="serializedAttributes">
/// The serialized attributes to use.
/// </param>
internal AttributesDictionary(AttributeSerializationRecord[]? serializedAttributes)
{
_backingDictionary = new Dictionary<string, object?>();

if (serializedAttributes is not null)
{
CultureInfo cultureInfo;

if (serializedAttributes
.FirstOrDefault(a => a.Key == "geoBlazorCulture") is { } serializedCulture)
{
string cultureName = serializedCulture.Value!;
cultureInfo = new CultureInfo(cultureName);
serializedAttributes = serializedAttributes.Where(a => a.Key != "geoBlazorCulture").ToArray();
}
else
{
cultureInfo = CultureInfo.DefaultThreadCurrentUICulture ?? CultureInfo.CurrentCulture;
}


foreach (AttributeSerializationRecord record in serializedAttributes)
{
switch (record.ValueType)
{
case "[object Number]":
_backingDictionary[record.Key] = double.Parse(record.Value!);
_backingDictionary[record.Key] = double.Parse(record.Value!, cultureInfo);

break;
case "[object Boolean]":
Expand All @@ -99,7 +132,7 @@ internal AttributesDictionary(AttributeSerializationRecord[]? serializedAttribut

break;
case "[object Date]":
_backingDictionary[record.Key] = DateTime.Parse(record.Value!);
_backingDictionary[record.Key] = DateTime.Parse(record.Value!, cultureInfo);

break;
default:
Expand Down
5 changes: 1 addition & 4 deletions src/dymaptic.GeoBlazor.Core/Scripts/dotNetBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,14 @@ import Domain from "@arcgis/core/layers/support/Domain";
import CodedValueDomain from "@arcgis/core/layers/support/CodedValueDomain";
import InheritedDomain from "@arcgis/core/layers/support/InheritedDomain";
import RangeDomain from "@arcgis/core/layers/support/RangeDomain";
import Effect = __esri.Effect;
import TileInfo from "@arcgis/core/layers/support/TileInfo";
import LOD from "@arcgis/core/layers/support/LOD";
import LayerSearchSource from "@arcgis/core/widgets/Search/LayerSearchSource";
import SearchSource from "@arcgis/core/widgets/Search/SearchSource";
import LocatorSearchSource from "@arcgis/core/widgets/Search/LocatorSearchSource";
import SearchResult = __esri.SearchResult;
import SuggestResult = __esri.SuggestResult;
import FeatureType from "@arcgis/core/layers/support/FeatureType";
import FeatureTemplate from "@arcgis/core/layers/support/FeatureTemplate";
import FeatureTemplateThumbnail = __esri.FeatureTemplateThumbnail;
import LabelClass from "@arcgis/core/layers/support/LabelClass";
import Renderer from "@arcgis/core/renderers/Renderer";
import PieChartRenderer from "@arcgis/core/renderers/PieChartRenderer";
Expand All @@ -120,7 +117,6 @@ import PieChartScheme = __esri.PieChartScheme;
import SizeScheme = __esri.SizeScheme;
import SizeSchemeForPoint = __esri.SizeSchemeForPoint;
import SizeSchemeForPolygon = __esri.SizeSchemeForPolygon;
import UniqueValuesResult = __esri.UniqueValuesResult;
import AuthoringInfo from "@arcgis/core/renderers/support/AuthoringInfo";
import AddressCandidate = __esri.AddressCandidate;
import FeatureSet from "@arcgis/core/rest/support/FeatureSet";
Expand All @@ -146,6 +142,7 @@ export function buildDotNetGraphic(graphic: Graphic): DotNetGraphic | null {

dotNetGraphic.uid = (graphic as any).uid;
dotNetGraphic.attributes = graphic.attributes ?? {};
dotNetGraphic.attributes.geoBlazorCulture = navigator.language;
if (graphic.symbol !== undefined && graphic.symbol !== null) {
dotNetGraphic.symbol = buildDotNetSymbol(graphic.symbol);
}
Expand Down
2 changes: 2 additions & 0 deletions src/dymaptic.GeoBlazor.Core/dymaptic.GeoBlazor.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
<InternalsVisibleTo Include="dymaptic.GeoBlazor.Core.Sample.Shared" />
<InternalsVisibleTo Include="dymaptic.GeoBlazor.Core.Test" />
<InternalsVisibleTo Include="dymaptic.GeoBlazor.Pro.Test.Unit" />
<InternalsVisibleTo Include="dymaptic.GeoBlazor.Core.Test.Blazor.Shared" />
<InternalsVisibleTo Include="dymaptic.GeoBlazor.Pro.Test.Blazor.Shared" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using dymaptic.GeoBlazor.Core.Objects;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Globalization;
using System.Runtime.Serialization;


namespace dymaptic.GeoBlazor.Core.Test.Blazor.Shared.Components;

public class LocalizationTests: TestRunnerBase
{
[TestMethod]
public void TestCanDeserializeAmericanDoubleAttribute()
{
string originalCulture = Thread.CurrentThread.CurrentCulture.Name;
SetCulture("en-US");
AttributeSerializationRecord doubleAttribute = new("Value", "1.1", "[object Number]");
AttributesDictionary attributes = new([doubleAttribute]);
Assert.AreEqual(1.1, attributes["Value"]);
SetCulture(originalCulture);
}

[TestMethod]
public void TestCanDeserializeEuropeanDoubleAttribute()
{
string originalCulture = Thread.CurrentThread.CurrentCulture.Name;
SetCulture("de-DE");
AttributeSerializationRecord doubleAttribute = new("Value", "1,1", "[object Number]");
AttributesDictionary attributes = new([doubleAttribute]);
Assert.AreEqual(1.1, attributes["Value"]);
SetCulture(originalCulture);
}

[TestMethod]
public void TestDeserializeAmericanDoubleAttributeInEuropeanCultureReturnsWrongValue()
{
string originalCulture = Thread.CurrentThread.CurrentCulture.Name;
SetCulture("de-DE");
AttributeSerializationRecord doubleAttribute = new("Value", "1.1", "[object Number]");
AttributesDictionary attributes = new([doubleAttribute]);
Assert.AreEqual(11.0, attributes["Value"]);
SetCulture(originalCulture);
}

[TestMethod]
public void TestDeserializeEuropeanDoubleAttributeInAmericanCultureReturnsWrongValue()
{
string originalCulture = Thread.CurrentThread.CurrentCulture.Name;
SetCulture("en-US");
AttributeSerializationRecord doubleAttribute = new("Value", "1,1", "[object Number]");
AttributesDictionary attributes = new([doubleAttribute]);
Assert.AreEqual(11.0, attributes["Value"]);
SetCulture(originalCulture);
}

private void SetCulture(string cultureName)
{
Thread.CurrentThread.CurrentCulture = new CultureInfo(cultureName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,29 @@
try
{
bool hasParameters = methodInfo.GetParameters().Any();
if (hasParameters)
if (methodInfo.ReturnType != typeof(Task))
{
void RenderHandler() => _methodsWithRenderedMaps.Add(methodInfo.Name);
await (Task)methodInfo.Invoke(this, [(Action)RenderHandler])!;
if (hasParameters)
{
void RenderHandler() => _methodsWithRenderedMaps.Add(methodInfo.Name);
methodInfo.Invoke(this, [(Action)RenderHandler]);
}
else
{
methodInfo.Invoke(this, null);
}
}
else
{
await (Task)methodInfo.Invoke(this, null)!;
if (hasParameters)
{
void RenderHandler() => _methodsWithRenderedMaps.Add(methodInfo.Name);
await (Task)methodInfo.Invoke(this, [(Action)RenderHandler])!;
}
else
{
await (Task)methodInfo.Invoke(this, null)!;
}
}

_passed[methodInfo.Name] = _resultBuilder.ToString();
Expand Down
Loading