dotnet / dotnet/command-line-api

Make commandline arguments support different locales with an IFormatProvider

Open
#1,733 13 comments 0 reactions 0 assignees View on GitHub
Area-Localization needs discussion
Dominant language
C#
Stars
3.7k
Forks
428
PR merge metrics
No merged PRs in 30d

Description

The following tests fail in my `nl_NL` locale:

* [Values_can_be_correctly_converted_to_decimal_without_the_parser_specifying_a_custom_converter](https://github.com/dotnet/command-line-api/blob/55dbf39074a7563e25e3409c9feb7afb8c8d2cc4/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs#L488)
* [Values_can_be_correctly_converted_to_double_without_the_parser_specifying_a_custom_converter](https://github.com/dotnet/command-line-api/blob/55dbf39074a7563e25e3409c9feb7afb8c8d2cc4/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs#L510)
* [Values_can_be_correctly_converted_to_float_without_the_parser_specifying_a_custom_converter](https://github.com/dotnet/command-line-api/blob/55dbf39074a7563e25e3409c9feb7afb8c8d2cc4/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs#L530)
* [Values_can_be_correctly_converted_to_nullable_decimal_without_the_parser_specifying_a_custom_converter](https://github.com/dotnet/command-line-api/blob/55dbf39074a7563e25e3409c9feb7afb8c8d2cc4/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs#L500)
* [Values_can_be_correctly_converted_to_nullable_double_without_the_parser_specifying_a_custom_converter](https://github.com/dotnet/command-line-api/blob/55dbf39074a7563e25e3409c9feb7afb8c8d2cc4/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs#L520)
* [Values_can_be_correctly_converted_to_nullable_float_without_the_parser_specifying_a_custom_converter](https://github.com/dotnet/command-line-api/blob/55dbf39074a7563e25e3409c9feb7afb8c8d2cc4/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs#L540)

This is because in Europe we write `123.456,78` as opposed to the US `123,456.78` format.

I have forked the project and made an attempt to fix this by accepting an `IFormatProvider` in the `TryConvertString` delegate; I'm quite sure this is the "correct" way to go (or at least something similar to this), but that turned out to be a bit more of a rabbithole than I currently have time for.

Here's my proposed change in `ArgumentConverter.StringConverters.cs`

```c#
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.Globalization;
using System.IO;

namespace System.CommandLine.Binding;

internal static partial class ArgumentConverter
{
private delegate bool TryConvertString(string token, IFormatProvider formatProvider, out object? value);

private static readonly Dictionary _stringConverters = new()
{
[typeof(bool)] = (string token, IFormatProvider formatProvider, out object? value) =>
{
if (bool.TryParse(token, out var parsed))
{
value = parsed;
return true;
}

value = default;
return false;
},

[typeof(DateTime)] = (string input, IFormatProvider formatProvider, out object? value) =>
{
if (DateTime.TryParse(input, formatProvider, DateTimeStyles.AssumeLocal, out var parsed))
{
value = parsed;
return true;
}

value = default;
return false;
},

[typeof(DateTimeOffset)] = (string input, IFormatProvider formatProvider, out object? value) =>
{
if (DateTimeOffset.TryParse(input, formatProvider, DateTimeStyles.AssumeLocal, out var parsed))
{
value = parsed;
return true;
}

value = default;
return false;
},

[typeof(decimal)] = (string input, IFormatProvider formatProvider, out object? value) =>
{
if (decimal.TryParse(input, NumberStyles.Float, formatProvider, out var parsed))
{
value = parsed;
return true;
}

value = default;
return false;
},

[typeof(DirectoryInfo)] = (string path, IFormatProvider formatProvider, out object? value) =>
{
if (string.IsNullOrEmpty(path))
{
value = default;
return false;
}
value = new DirectoryInfo(path);
return true;
},

[typeof(double)] = (string input, IFormatProvider formatProvider, out object? value) =>
{
if (double.TryParse(input, NumberStyles.Float, formatProvider, out var parsed))
{
value = parsed;
return true;
}

value = default;
return false;
},

[typeof(FileInfo)] = (string path, IFormatProvider formatProvider, out object? value) =>
{
if (string.IsNullOrEmpty(path))
{
value = default;
return false;
}
value = new FileInfo(path);
return true;
},

[typeof(FileSystemInfo)] = (string path, IFormatProvider formatProvider, out object? value) =>
{
if (string.IsNullOrEmpty(path))
{
value = default;
return false;
}
if (Directory.Exists(path))
{
value = new DirectoryInfo(path);
}
else if (path.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) ||
path.EndsWith(Path.AltDirectorySeparatorChar.ToString(), StringComparison.Ordinal))
{
value = new DirectoryInfo(path);
}
else
{
value = new FileInfo(path);
}

return true;
},

[typeof(float)] = (string input, IFormatProvider formatProvider, out object? value) =>
{
if (float.TryParse(input, NumberStyles.Float, formatProvider, out var parsed))
{
value = parsed;
return true;
}

value = default;
return false;
},

[typeof(Guid)] = (string input, IFormatProvider formatProvider, out object? value) =>
{
if (Guid.TryParse(input, out var parsed))
{
value = parsed;
return true;
}

value = default;
return false;
},

[typeof(int)] = (string token, IFormatProvider formatProvider, out object? value) =>
{
if (int.TryParse(token, NumberStyles.Integer, formatProvider, out var intValue))
{
value = intValue;
return true;
}

value = default;
return false;
},

[typeof(long)] = (string token, IFormatProvider formatProvider, out object? value) =>
{
if (long.TryParse(token, NumberStyles.Integer, formatProvider, out var longValue))
{
value = longValue;
return true;
}

value = default;
return false;
},

[typeof(short)] = (string token, IFormatProvider formatProvider, out object? value) =>
{
if (short.TryParse(token, NumberStyles.Integer, formatProvider, out var shortValue))
{
value = shortValue;
return true;
}

value = default;
return false;
},

[typeof(uint)] = (string token, IFormatProvider formatProvider, out object? value) =>
{
if (uint.TryParse(token, NumberStyles.Integer, formatProvider, out var uintValue))
{
value = uintValue;
return true;
}

value = default;
return false;
},

[typeof(sbyte)] = (string token, IFormatProvider formatProvider, out object? value) =>
{
if (sbyte.TryParse(token, NumberStyles.Integer, formatProvider, out var sbyteValue))
{
value = sbyteValue;
return true;
}

value = default;
return false;
},

[typeof(byte)] = (string token, IFormatProvider formatProvider, out object? value) =>
{
if (byte.TryParse(token, NumberStyles.Integer, formatProvider, out var byteValue))
{
value = byteValue;
return true;
}

value = default;
return false;
},

[typeof(string)] = (string input, IFormatProvider formatProvider, out object? value) =>
{
value = input;
return true;
},

[typeof(ulong)] = (string token, IFormatProvider formatProvider, out object? value) =>
{
if (ulong.TryParse(token, NumberStyles.Integer, formatProvider, out var ulongValue))
{
value = ulongValue;
return true;
}

value = default;
return false;
},

[typeof(ushort)] = (string token, IFormatProvider formatProvider, out object? value) =>
{
if (ushort.TryParse(token, NumberStyles.Integer, formatProvider, out var ushortValue))
{
value = ushortValue;
return true;
}

value = default;
return false;
},

[typeof(Uri)] = (string input, IFormatProvider formatProvider, out object? value) =>
{
if (Uri.TryCreate(input, UriKind.RelativeOrAbsolute, out var uri))
{
value = uri;
return true;
}

value = default;
return false;
},

[typeof(TimeSpan)] = (string input, IFormatProvider formatProvider, out object? value) =>
{
if (TimeSpan.TryParse(input, formatProvider, out var timeSpan))
{
value = timeSpan;
return true;
}

value = default;
return false;
},
};
}
```

I think the above can serve as a startingpoint.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.