Skip to content

Make commandline arguments support different locales with an IFormatProvider #1733

Open
@RobThree

Description

@RobThree

The following tests fail in my nl_NL locale:

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`
// 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<Type, TryConvertString> _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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions