From 589241df803eb4c7b30823eb2fbb7cce691dea56 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Mon, 12 Oct 2026 21:37:47 +0100 Subject: [PATCH] fix: format numbers with the current culture - Align unhinted Single formatting with nullable values, hints, and parsing. - Generate numeric formatting with the same culture as runtime converters. - Set en-US as the shared TUnit culture and isolate French culture regressions. - Make converter test inputs and expectations follow the selected culture. - Demonstrate numeric formatting with a comma decimal separator. Closes #175 --- CLAUDE.md | 1 + src/Directory.Build.props | 4 + .../Converter/SingleToStringTypeConverter.cs | 6 +- .../NumericStringConversionPlugin.cs | 5 +- .../Pages/converters/CultureExamples.cs | 44 +++++++ .../Documentation/Pages/converters/Program.cs | 2 + .../NumericCultureConversionTests.cs | 93 ++++++++++++++ ...indingTypeConverterTryConvertTypedTests.cs | 2 +- .../DecimalToStringTypeConverterTests.cs | 6 +- .../DoubleToStringTypeConverterTests.cs | 2 +- ...llableDecimalToStringTypeConverterTests.cs | 6 +- ...ullableDoubleToStringTypeConverterTests.cs | 2 +- ...ullableSingleToStringTypeConverterTests.cs | 2 +- .../NumericToStringCultureTests.cs | 115 ++++++++++++++++++ .../SingleToStringTypeConverterTests.cs | 32 ++--- .../StringToDecimalTypeConverterTests.cs | 6 +- .../StringToDoubleTypeConverterTests.cs | 8 +- ...ringToNullableDecimalTypeConverterTests.cs | 2 +- ...tringToNullableDoubleTypeConverterTests.cs | 4 +- ...tringToNullableSingleTypeConverterTests.cs | 2 +- .../StringToSingleTypeConverterTests.cs | 8 +- 21 files changed, 301 insertions(+), 51 deletions(-) create mode 100644 src/examples/Documentation/Pages/converters/CultureExamples.cs create mode 100644 src/tests/ReactiveUI.Binding.SourceGenerators.Tests/NumericCultureConversionTests.cs create mode 100644 src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NumericToStringCultureTests.cs diff --git a/CLAUDE.md b/CLAUDE.md index 3ca46be2..e08035aa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -115,6 +115,7 @@ dotnet clean ReactiveUI.Binding.SourceGenerators.slnf The working folder must be `./src` folder. **IMPORTANT:** +- All TUnit test projects use `en-US` for the current culture and UI culture, configured by an assembly attribute in `Directory.Build.props`. Culture-specific regression tests use TUnit's `Culture` attribute to override it for that test. - Do NOT use `--no-build` flag when running tests. Always build before testing to ensure all code changes are compiled. - Use `--output Detailed` to see Console.WriteLine output from tests (place BEFORE any `--` separator). diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 01b70b16..36e334ef 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -119,6 +119,10 @@ + + + <_Parameter1>en-US + $(AssemblyName).testconfig.json PreserveNewest diff --git a/src/ReactiveUI.Binding.Shared/Bindings/Converter/SingleToStringTypeConverter.cs b/src/ReactiveUI.Binding.Shared/Bindings/Converter/SingleToStringTypeConverter.cs index 540a7261..16536af7 100644 --- a/src/ReactiveUI.Binding.Shared/Bindings/Converter/SingleToStringTypeConverter.cs +++ b/src/ReactiveUI.Binding.Shared/Bindings/Converter/SingleToStringTypeConverter.cs @@ -9,9 +9,9 @@ namespace ReactiveUI.Binding; #endif /// -/// Converts a to a using the invariant culture, as generated bindings do. An +/// Converts a to a using the current culture. An /// hint gives the number of decimal places (the F format) and a hint gives -/// the format string; both use the current culture, and a malformed format throws . +/// the format string; a malformed format throws . /// public sealed class SingleToStringTypeConverter : BindingTypeConverter { @@ -40,7 +40,7 @@ public override bool TryConvert(float from, object? conversionHint, [NotNullWhen default: { - result = from.ToString(System.Globalization.CultureInfo.InvariantCulture); + result = from.ToString(System.Globalization.CultureInfo.CurrentCulture); return true; } } diff --git a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Conversion/NumericStringConversionPlugin.cs b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Conversion/NumericStringConversionPlugin.cs index 3f2d098e..a806ae3f 100644 --- a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Conversion/NumericStringConversionPlugin.cs +++ b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Conversion/NumericStringConversionPlugin.cs @@ -24,7 +24,7 @@ internal sealed class NumericStringConversionPlugin : IConversionPlugin : null; } - /// Preserves width, precision, custom format and the Single converter's invariant default. + /// Preserves width, precision and custom format using the current culture. /// The unwrapped numeric type. /// Whether the input has a nullable wrapper. /// The concrete ToString operation. @@ -32,8 +32,7 @@ internal static string FormattingExpression(ITypeSymbol type, bool nullable) { var value = nullable ? "__value.Value" : "__value"; var prefix = type.SpecialType is SpecialType.System_Single or SpecialType.System_Double or SpecialType.System_Decimal ? "F" : "D"; - var culture = type.SpecialType == SpecialType.System_Single && !nullable ? "global::System.Globalization.CultureInfo.InvariantCulture" : string.Empty; return $"__hint is int __precision ? {value}.ToString(\"{prefix}\" + __precision.ToString())" - + $" : __hint is string __format ? {value}.ToString(__format) : {value}.ToString({culture})"; + + $" : __hint is string __format ? {value}.ToString(__format) : {value}.ToString()"; } } diff --git a/src/examples/Documentation/Pages/converters/CultureExamples.cs b/src/examples/Documentation/Pages/converters/CultureExamples.cs new file mode 100644 index 00000000..bec233bd --- /dev/null +++ b/src/examples/Documentation/Pages/converters/CultureExamples.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Globalization; + +namespace ReactiveUI.Binding.Documentation.Converters; + +/// Demonstrates the culture used by numeric formatting. +public static class CultureExamples +{ + /// Formats a float and its nullable counterpart with the current culture, including format hints. + public static void FormatSingleWithCurrentCulture() + { + CultureInfo? previousCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR"); + const float value = 3.14F; + SingleToStringTypeConverter converter = new SingleToStringTypeConverter(); + NullableSingleToStringTypeConverter nullableConverter = new NullableSingleToStringTypeConverter(); + + _ = converter.TryConvert(value, conversionHint: null, out var plain); + _ = converter.TryConvert(value, conversionHint: 2, out var precision); + _ = converter.TryConvert(value, conversionHint: "F3", out var formatted); + _ = nullableConverter.TryConvert(value, conversionHint: null, out var nullable); + + Console.WriteLine(plain); + Console.WriteLine(precision); + Console.WriteLine(formatted); + Console.WriteLine(nullable); + + // Output: + // 3,14 + // 3,14 + // 3,140 + // 3,14 + } + finally + { + CultureInfo.CurrentCulture = previousCulture; + } + } +} diff --git a/src/examples/Documentation/Pages/converters/Program.cs b/src/examples/Documentation/Pages/converters/Program.cs index b55345e6..b50d7135 100644 --- a/src/examples/Documentation/Pages/converters/Program.cs +++ b/src/examples/Documentation/Pages/converters/Program.cs @@ -40,6 +40,8 @@ NumberExamples.FormatStudentScore(); +CultureExamples.FormatSingleWithCurrentCulture(); + NumberExamples.ParseEulersNumber(); NumberExamples.FormatEulersNumber(); diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/NumericCultureConversionTests.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/NumericCultureConversionTests.cs new file mode 100644 index 00000000..e4fe2266 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/NumericCultureConversionTests.cs @@ -0,0 +1,93 @@ +// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Reflection; +using Microsoft.CodeAnalysis.CSharp; +using ReactiveUI.Binding.SourceGenerators.Tests.Helpers; + +namespace ReactiveUI.Binding.SourceGenerators.Tests; + +/// Verifies numeric formatting follows the consumer's culture in generated and runtime bindings. +public class NumericCultureConversionTests +{ + /// Checks default, precision, format and unsupported hints for nullable and nonnullable values. + /// The numeric source type. + /// The runtime converter name stem. + /// The source value expression. + /// A task representing the asynchronous test. + [Test] + [Arguments("float", "Single", "7.25f")] + public async Task NumericBindings_UseCurrentCulture(string type, string name, string value) + { + var result = TestHelper.RunGenerator(Scenario(type, name, value), LanguageVersion.CSharp10); + await result.CompilationSucceeds(); + await result.GeneratedSourceDoesNotContain("BindToDispatch.g.cs", "InvariantCulture"); + var (assembly, context) = TestHelper.EmitAndLoad(result); + try + { + var run = assembly.GetType("Usage")!.GetMethod("Run", BindingFlags.Public | BindingFlags.Static)!; + await Assert.That((bool)run.Invoke(null, null)!).IsTrue(); + } + finally + { + context.Unload(); + } + } + + /// Builds formatting and round-trip probes with explicit culture-sensitive expectations. + /// The numeric source type. + /// The runtime converter name stem. + /// The source value expression. + /// The executable consumer source. + private static string Scenario(string type, string name, string value) => $$""" + using System; + using System.Globalization; + using ReactiveUI.Binding; + public class Target { public string Value { get; set; } } + public class NumericTarget { public {{type}} Value { get; set; } } + public static class Usage + { + public static bool Run() + { + var previous = CultureInfo.CurrentCulture; + try + { + foreach (var culture in new[] { "en-US", "fr-FR" }) + { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(culture); + {{type}} value = {{value}}; + foreach (object hint in new object[] { null, 3, "N2", new object() }) + { + var expected = hint is int ? value.ToString("F" + (int)hint) + : hint is string ? value.ToString((string)hint) : value.ToString(); + var target = new Target(); + IObservable<{{type}}> values = new global::ReactiveUI.Primitives.Advanced.ImmediateReturnSignal<{{type}}>(value); + using (values.BindTo(target, x => x.Value, conversionHint: hint)) Check(expected, target.Value); + string formatted; + new {{name}}ToStringTypeConverter().TryConvert(value, hint, out formatted); + Check(expected, formatted); + IObservable<{{type}}?> nullableValues = new global::ReactiveUI.Primitives.Advanced.ImmediateReturnSignal<{{type}}?>(value); + using (nullableValues.BindTo(target, x => x.Value, conversionHint: hint)) Check(expected, target.Value); + new Nullable{{name}}ToStringTypeConverter().TryConvert(value, hint, out formatted); + Check(expected, formatted); + var roundTrip = new NumericTarget(); + IObservable text = new global::ReactiveUI.Primitives.Advanced.ImmediateReturnSignal(target.Value); + using (text.BindTo(roundTrip, x => x.Value)) + if (roundTrip.Value != value) throw new InvalidOperationException("Generated numeric round trip failed: " + target.Value); + {{type}} parsed; + if (!new StringTo{{name}}TypeConverter().TryConvert(formatted, null, out parsed) || parsed != value) + throw new InvalidOperationException("Runtime numeric round trip failed: " + formatted); + } + } + } + finally { CultureInfo.CurrentCulture = previous; } + return true; + } + private static void Check(string expected, string actual) + { + if (actual != expected) throw new InvalidOperationException(CultureInfo.CurrentCulture.Name + ": expected=" + expected + ", actual=" + actual); + } + } + """; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Bindings/BindingTypeConverterTryConvertTypedTests.cs b/src/tests/ReactiveUI.Binding.Tests/Bindings/BindingTypeConverterTryConvertTypedTests.cs index e9b037ef..5d86b5f0 100644 --- a/src/tests/ReactiveUI.Binding.Tests/Bindings/BindingTypeConverterTryConvertTypedTests.cs +++ b/src/tests/ReactiveUI.Binding.Tests/Bindings/BindingTypeConverterTryConvertTypedTests.cs @@ -172,7 +172,7 @@ public Task ShortToString_Failure_Null() => [Test] [MethodImpl(MethodImplOptions.AggressiveInlining)] public Task SingleToString_Success() => - AssertConverterSuccess(new SingleToStringTypeConverter(), SingleValue, SingleValue.ToString(System.Globalization.CultureInfo.InvariantCulture)); + AssertConverterSuccess(new SingleToStringTypeConverter(), SingleValue, SingleValue.ToString(System.Globalization.CultureInfo.CurrentCulture)); /// Verifies SingleToStringTypeConverter fails when given a wrong type. /// A task representing the asynchronous test operation. diff --git a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/DecimalToStringTypeConverterTests.cs b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/DecimalToStringTypeConverterTests.cs index 0851f1d6..cc0f3273 100644 --- a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/DecimalToStringTypeConverterTests.cs +++ b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/DecimalToStringTypeConverterTests.cs @@ -31,7 +31,7 @@ public async Task TryConvert_DecimalToString_Succeeds() var result = converter.TryConvert(value, null, out var output); await Assert.That(result).IsTrue(); - await Assert.That(output).IsEqualTo("123.456"); + await Assert.That(output).IsEqualTo(value.ToString(System.Globalization.CultureInfo.CurrentCulture)); } /// Verifies TryConvert MaxValue Succeeds. @@ -73,7 +73,7 @@ public async Task TryConvert_NegativeValue_Succeeds() var result = converter.TryConvert(value, null, out var output); await Assert.That(result).IsTrue(); - await Assert.That(output).IsEqualTo("-123.456"); + await Assert.That(output).IsEqualTo(value.ToString(System.Globalization.CultureInfo.CurrentCulture)); } /// Verifies TryConvert WithConversionHint FormatsCorrectly. @@ -87,7 +87,7 @@ public async Task TryConvert_WithConversionHint_FormatsCorrectly() var result = converter.TryConvert(value, ExpectedAffinity, out var output); await Assert.That(result).IsTrue(); - await Assert.That(output).IsEqualTo("42.50"); + await Assert.That(output).IsEqualTo(value.ToString("F2", System.Globalization.CultureInfo.CurrentCulture)); } /// Verifies TryConvert WithStringFormatHint CurrencyFormat. diff --git a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/DoubleToStringTypeConverterTests.cs b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/DoubleToStringTypeConverterTests.cs index da1a7a7b..eb9cd554 100644 --- a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/DoubleToStringTypeConverterTests.cs +++ b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/DoubleToStringTypeConverterTests.cs @@ -87,7 +87,7 @@ public async Task TryConvert_WithConversionHint_FormatsCorrectly() var result = converter.TryConvert(value, ExpectedAffinity, out var output); await Assert.That(result).IsTrue(); - await Assert.That(output).IsEqualTo("42.50"); + await Assert.That(output).IsEqualTo(value.ToString("F2", System.Globalization.CultureInfo.CurrentCulture)); } /// Verifies TryConvert WithStringFormatHint CustomPrecision. diff --git a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NullableDecimalToStringTypeConverterTests.cs b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NullableDecimalToStringTypeConverterTests.cs index 28452ac9..8bad94d2 100644 --- a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NullableDecimalToStringTypeConverterTests.cs +++ b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NullableDecimalToStringTypeConverterTests.cs @@ -38,7 +38,7 @@ public async Task TryConvert_DecimalNullableToString_Succeeds() var result = converter.TryConvert(SampleDecimal, null, out var output); await Assert.That(result).IsTrue(); - await Assert.That(output).IsEqualTo("123.456"); + await Assert.That(output).IsEqualTo(SampleDecimal.ToString(System.Globalization.CultureInfo.CurrentCulture)); } /// Verifies TryConvert MaxValue Succeeds. @@ -78,7 +78,7 @@ public async Task TryConvert_NegativeValue_Succeeds() var result = converter.TryConvert(NegativeDecimal, null, out var output); await Assert.That(result).IsTrue(); - await Assert.That(output).IsEqualTo("-123.456"); + await Assert.That(output).IsEqualTo(NegativeDecimal.ToString(System.Globalization.CultureInfo.CurrentCulture)); } /// Verifies TryConvert NullValue ReturnsTrue. @@ -101,7 +101,7 @@ public async Task TryConvert_WithConversionHint_FormatsCorrectly() var result = converter.TryConvert(FormattedDecimal, ExpectedAffinity, out var output); await Assert.That(result).IsTrue(); - await Assert.That(output).IsEqualTo("42.50"); + await Assert.That(output).IsEqualTo(FormattedDecimal.ToString("F2", System.Globalization.CultureInfo.CurrentCulture)); } /// Verifies TryConvert Zero Succeeds. diff --git a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NullableDoubleToStringTypeConverterTests.cs b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NullableDoubleToStringTypeConverterTests.cs index ae5a71b8..2ea2d808 100644 --- a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NullableDoubleToStringTypeConverterTests.cs +++ b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NullableDoubleToStringTypeConverterTests.cs @@ -99,7 +99,7 @@ public async Task TryConvert_WithConversionHint_FormatsCorrectly() var result = converter.TryConvert(FormattedDouble, ExpectedAffinity, out var output); await Assert.That(result).IsTrue(); - await Assert.That(output).IsEqualTo("42.50"); + await Assert.That(output).IsEqualTo(FormattedDouble.ToString("F2", System.Globalization.CultureInfo.CurrentCulture)); } /// Verifies TryConvert with a string format hint formats correctly. diff --git a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NullableSingleToStringTypeConverterTests.cs b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NullableSingleToStringTypeConverterTests.cs index 55cee552..6491faed 100644 --- a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NullableSingleToStringTypeConverterTests.cs +++ b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NullableSingleToStringTypeConverterTests.cs @@ -99,7 +99,7 @@ public async Task TryConvert_WithConversionHint_FormatsCorrectly() var result = converter.TryConvert(FormattedSingle, ExpectedAffinity, out var output); await Assert.That(result).IsTrue(); - await Assert.That(output).IsEqualTo("42.50"); + await Assert.That(output).IsEqualTo(FormattedSingle.ToString("F2", System.Globalization.CultureInfo.CurrentCulture)); } /// Verifies TryConvert with a string format hint formats correctly. diff --git a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NumericToStringCultureTests.cs b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NumericToStringCultureTests.cs new file mode 100644 index 00000000..901af7bd --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/NumericToStringCultureTests.cs @@ -0,0 +1,115 @@ +// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Globalization; +using TUnit.Core.Executors; + +namespace ReactiveUI.Binding.Tests.Bindings.TypeConverters; + +/// Verifies numeric formatting follows the test's culture in both runtime flavours. +public class NumericToStringCultureTests +{ + /// The culture shared by ordinary tests. + private const string CommonCulture = "en-US"; + + /// Verifies ordinary tests start with the common assembly culture. + /// A task representing the asynchronous operation. + [Test] + public async Task TestCulture_WithoutOverride_UsesEnglishCulture() + { + await Assert.That(CultureInfo.CurrentCulture.Name).IsEqualTo(CommonCulture); + await Assert.That(CultureInfo.CurrentUICulture.Name).IsEqualTo(CommonCulture); + } + + /// Formats default, precision and string hints with a comma decimal separator. + /// Whether to use the System.Reactive runtime. + /// Whether the source is nullable. + /// The source numeric type. + /// The formatting hint. + /// A task representing the asynchronous operation. + [Test] + [Culture("fr-FR")] + [MatrixDataSource] + public async Task TryConvert_FrenchCulture_UsesCurrentCulture( + [Matrix(false, true)] bool reactive, + [Matrix(false, true)] bool nullable, + [Matrix("Single", "Double", "Decimal")] string numericType, + [Matrix(null, 2, "F2")] object? hint) + { + await Assert.That(CultureInfo.CurrentCulture.Name).IsEqualTo("fr-FR"); + await AssertFormatting(reactive, nullable, numericType, hint, hint is null ? "42,5" : "42,50"); + } + + /// Formats default, precision and string hints with a dot decimal separator. + /// Whether to use the System.Reactive runtime. + /// Whether the source is nullable. + /// The source numeric type. + /// The formatting hint. + /// A task representing the asynchronous operation. + [Test] + [Culture(CommonCulture)] + [MatrixDataSource] + public async Task TryConvert_EnglishCulture_UsesCurrentCulture( + [Matrix(false, true)] bool reactive, + [Matrix(false, true)] bool nullable, + [Matrix("Single", "Double", "Decimal")] string numericType, + [Matrix(null, 2, "F2")] object? hint) + { + await Assert.That(CultureInfo.CurrentCulture.Name).IsEqualTo(CommonCulture); + await AssertFormatting(reactive, nullable, numericType, hint, hint is null ? "42.5" : "42.50"); + } + + /// Checks one converter against the culture-specific expected text. + /// Whether to use the System.Reactive runtime. + /// Whether the source is nullable. + /// The source numeric type. + /// The formatting hint. + /// The expected text. + /// A task representing the asynchronous operation. + private static async Task AssertFormatting(bool reactive, bool nullable, string numericType, object? hint, string expected) + { + const float singleValue = 42.5F; + const double doubleValue = 42.5D; + const decimal decimalValue = 42.5M; + object value = numericType switch + { + "Single" => singleValue, + "Double" => doubleValue, + _ => decimalValue, + }; + + bool success; + object? output; + if (reactive) + { + global::ReactiveUI.Binding.Reactive.IBindingTypeConverter converter = (numericType, nullable) switch + { + ("Single", false) => new global::ReactiveUI.Binding.Reactive.SingleToStringTypeConverter(), + ("Single", true) => new global::ReactiveUI.Binding.Reactive.NullableSingleToStringTypeConverter(), + ("Double", false) => new global::ReactiveUI.Binding.Reactive.DoubleToStringTypeConverter(), + ("Double", true) => new global::ReactiveUI.Binding.Reactive.NullableDoubleToStringTypeConverter(), + ("Decimal", false) => new global::ReactiveUI.Binding.Reactive.DecimalToStringTypeConverter(), + _ => new global::ReactiveUI.Binding.Reactive.NullableDecimalToStringTypeConverter(), + }; + success = converter.TryConvertTyped(value, hint, out output); + } + else + { + IBindingTypeConverter converter = (numericType, nullable) switch + { + ("Single", false) => new SingleToStringTypeConverter(), + ("Single", true) => new NullableSingleToStringTypeConverter(), + ("Double", false) => new DoubleToStringTypeConverter(), + ("Double", true) => new NullableDoubleToStringTypeConverter(), + ("Decimal", false) => new DecimalToStringTypeConverter(), + _ => new NullableDecimalToStringTypeConverter(), + }; + success = converter.TryConvertTyped(value, hint, out output); + } + + await Assert.That(success).IsTrue(); + await Assert.That(output).IsEqualTo(expected); + await Assert.That(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator).IsEqualTo(expected.Contains(',') ? "," : "."); + } +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/SingleToStringTypeConverterTests.cs b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/SingleToStringTypeConverterTests.cs index d108f699..33825271 100644 --- a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/SingleToStringTypeConverterTests.cs +++ b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/SingleToStringTypeConverterTests.cs @@ -34,7 +34,7 @@ public async Task TryConvert_MaxValue_Succeeds() var result = converter.TryConvert(value, null, out var output); await Assert.That(result).IsTrue(); - await Assert.That(output).IsEqualTo(float.MaxValue.ToString(System.Globalization.CultureInfo.InvariantCulture)); + await Assert.That(output).IsEqualTo(float.MaxValue.ToString(System.Globalization.CultureInfo.CurrentCulture)); } /// Verifies TryConvert MinValue Succeeds. @@ -48,7 +48,7 @@ public async Task TryConvert_MinValue_Succeeds() var result = converter.TryConvert(value, null, out var output); await Assert.That(result).IsTrue(); - await Assert.That(output).IsEqualTo(float.MinValue.ToString(System.Globalization.CultureInfo.InvariantCulture)); + await Assert.That(output).IsEqualTo(float.MinValue.ToString(System.Globalization.CultureInfo.CurrentCulture)); } /// Verifies TryConvert NegativeValue Succeeds. @@ -62,7 +62,7 @@ public async Task TryConvert_NegativeValue_Succeeds() var result = converter.TryConvert(value, null, out var output); await Assert.That(result).IsTrue(); - await Assert.That(output).IsEqualTo(value.ToString(System.Globalization.CultureInfo.InvariantCulture)); + await Assert.That(output).IsEqualTo(value.ToString(System.Globalization.CultureInfo.CurrentCulture)); } /// Verifies TryConvert SingleToString Succeeds. @@ -76,28 +76,20 @@ public async Task TryConvert_SingleToString_Succeeds() var result = converter.TryConvert(value, null, out var output); await Assert.That(result).IsTrue(); - await Assert.That(output).IsEqualTo(value.ToString(System.Globalization.CultureInfo.InvariantCulture)); + await Assert.That(output).IsEqualTo(value.ToString(System.Globalization.CultureInfo.CurrentCulture)); } - /// Verifies TryConvert ignores a culture whose decimal separator is a comma, as generated bindings do. + /// Verifies TryConvert uses the current culture when its decimal separator is a comma. /// A task representing the asynchronous operation. [Test] - public async Task TryConvert_CommaDecimalCulture_UsesInvariantCulture() + [TUnit.Core.Executors.Culture("fr-FR")] + public async Task TryConvert_CommaDecimalCulture_UsesCurrentCulture() { var converter = new SingleToStringTypeConverter(); - var previous = System.Globalization.CultureInfo.CurrentCulture; - string? output; - try - { - System.Globalization.CultureInfo.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo("fr-FR"); - _ = converter.TryConvert(FractionalValue, null, out output); - } - finally - { - System.Globalization.CultureInfo.CurrentCulture = previous; - } - - await Assert.That(output).IsEqualTo("7.25"); + var result = converter.TryConvert(FractionalValue, null, out var output); + + await Assert.That(result).IsTrue(); + await Assert.That(output).IsEqualTo("7,25"); } /// Verifies TryConvert WithConversionHint FormatsCorrectly. @@ -111,7 +103,7 @@ public async Task TryConvert_WithConversionHint_FormatsCorrectly() var result = converter.TryConvert(value, ExpectedAffinity, out var output); await Assert.That(result).IsTrue(); - await Assert.That(output).IsEqualTo("42.50"); + await Assert.That(output).IsEqualTo(value.ToString("F2", System.Globalization.CultureInfo.CurrentCulture)); } /// Verifies TryConvert with a string format hint formats correctly. diff --git a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToDecimalTypeConverterTests.cs b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToDecimalTypeConverterTests.cs index 051e7ebf..11362911 100644 --- a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToDecimalTypeConverterTests.cs +++ b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToDecimalTypeConverterTests.cs @@ -58,7 +58,7 @@ public async Task TryConvert_StringToDecimal_Succeeds() { var converter = new StringToDecimalTypeConverter(); - var result = converter.TryConvert("123.456", null, out var output); + var result = converter.TryConvert(ParsedDecimal.ToString(System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsEqualTo(ParsedDecimal); @@ -97,7 +97,7 @@ public async Task TryConvert_NegativeValue_Succeeds() { var converter = new StringToDecimalTypeConverter(); - var result = converter.TryConvert("-123.456", null, out var output); + var result = converter.TryConvert(NegativeDecimal.ToString(System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsEqualTo(NegativeDecimal); @@ -110,7 +110,7 @@ public async Task TryConvertTyped_ValidString_Succeeds() { var converter = new StringToDecimalTypeConverter(); - var result = converter.TryConvertTyped("456.789", null, out var output); + var result = converter.TryConvertTyped(TypedDecimal.ToString(System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsEqualTo(TypedDecimal); diff --git a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToDoubleTypeConverterTests.cs b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToDoubleTypeConverterTests.cs index 05ae705a..f1adaaa5 100644 --- a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToDoubleTypeConverterTests.cs +++ b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToDoubleTypeConverterTests.cs @@ -61,7 +61,7 @@ public async Task TryConvert_ScientificNotation_Succeeds() { var converter = new StringToDoubleTypeConverter(); - var result = converter.TryConvert("1.23E+10", null, out var output); + var result = converter.TryConvert(ScientificDouble.ToString("E2", System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsEqualTo(ScientificDouble); @@ -74,7 +74,7 @@ public async Task TryConvert_StringToDouble_Succeeds() { var converter = new StringToDoubleTypeConverter(); - var result = converter.TryConvert("123.456", null, out var output); + var result = converter.TryConvert(ParsedDouble.ToString(System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsEqualTo(ParsedDouble); @@ -113,7 +113,7 @@ public async Task TryConvert_NegativeValue_Succeeds() { var converter = new StringToDoubleTypeConverter(); - var result = converter.TryConvert("-123.456", null, out var output); + var result = converter.TryConvert(NegativeDouble.ToString(System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsEqualTo(NegativeDouble); @@ -126,7 +126,7 @@ public async Task TryConvertTyped_ValidString_Succeeds() { var converter = new StringToDoubleTypeConverter(); - var result = converter.TryConvertTyped("456.789", null, out var output); + var result = converter.TryConvertTyped(TypedDouble.ToString(System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsEqualTo(TypedDouble); diff --git a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToNullableDecimalTypeConverterTests.cs b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToNullableDecimalTypeConverterTests.cs index 3c23b97a..5a9666aa 100644 --- a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToNullableDecimalTypeConverterTests.cs +++ b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToNullableDecimalTypeConverterTests.cs @@ -52,7 +52,7 @@ public async Task TryConvert_StringToDecimalNullable_Succeeds() { var converter = new StringToNullableDecimalTypeConverter(); - var result = converter.TryConvert("123.456", null, out var output); + var result = converter.TryConvert(ParsedDecimal.ToString(System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsEqualTo(ParsedDecimal); diff --git a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToNullableDoubleTypeConverterTests.cs b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToNullableDoubleTypeConverterTests.cs index 5e494ce5..897f632d 100644 --- a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToNullableDoubleTypeConverterTests.cs +++ b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToNullableDoubleTypeConverterTests.cs @@ -55,7 +55,7 @@ public async Task TryConvert_ScientificNotation_Succeeds() { var converter = new StringToNullableDoubleTypeConverter(); - var result = converter.TryConvert("1.23E+10", null, out var output); + var result = converter.TryConvert(ScientificDouble.ToString("E2", System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsEqualTo(ScientificDouble); @@ -68,7 +68,7 @@ public async Task TryConvert_StringToDoubleNullable_Succeeds() { var converter = new StringToNullableDoubleTypeConverter(); - var result = converter.TryConvert("123.456", null, out var output); + var result = converter.TryConvert(ParsedDouble.ToString(System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsEqualTo(ParsedDouble); diff --git a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToNullableSingleTypeConverterTests.cs b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToNullableSingleTypeConverterTests.cs index 40ebbfd9..eff0b84c 100644 --- a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToNullableSingleTypeConverterTests.cs +++ b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToNullableSingleTypeConverterTests.cs @@ -55,7 +55,7 @@ public async Task TryConvert_StringToSingleNullable_Succeeds() { var converter = new StringToNullableSingleTypeConverter(); - var result = converter.TryConvert("123.456", null, out var output); + var result = converter.TryConvert(ParsedSingle.ToString(System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsNotNull(); diff --git a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToSingleTypeConverterTests.cs b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToSingleTypeConverterTests.cs index 65485305..ae90343e 100644 --- a/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToSingleTypeConverterTests.cs +++ b/src/tests/ReactiveUI.Binding.Tests/Bindings/TypeConverters/StringToSingleTypeConverterTests.cs @@ -67,7 +67,7 @@ public async Task TryConvert_StringToSingle_Succeeds() { var converter = new StringToSingleTypeConverter(); - var result = converter.TryConvert("123.456", null, out var output); + var result = converter.TryConvert(ParsedSingle.ToString(System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsEqualTo(ParsedSingle).Within(SingleTolerance); @@ -106,7 +106,7 @@ public async Task TryConvert_NegativeValue_Succeeds() { var converter = new StringToSingleTypeConverter(); - var result = converter.TryConvert("-123.456", null, out var output); + var result = converter.TryConvert(NegativeSingle.ToString(System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsEqualTo(NegativeSingle).Within(SingleTolerance); @@ -119,7 +119,7 @@ public async Task TryConvert_ScientificNotation_Succeeds() { var converter = new StringToSingleTypeConverter(); - var result = converter.TryConvert("1.23E+5", null, out var output); + var result = converter.TryConvert(ScientificSingle.ToString("E2", System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsEqualTo(ScientificSingle).Within(ScientificTolerance); @@ -132,7 +132,7 @@ public async Task TryConvertTyped_ValidString_Succeeds() { var converter = new StringToSingleTypeConverter(); - var result = converter.TryConvertTyped("456.789", null, out var output); + var result = converter.TryConvertTyped(TypedSingle.ToString(System.Globalization.CultureInfo.CurrentCulture), null, out var output); await Assert.That(result).IsTrue(); await Assert.That(output).IsTypeOf();