-1

How can I convert a string with a month name like "agosto" to the english translation "August" without knowing if "agosto" is spanish or italian?

I know that I get the month name for the month number 8 in Spanish with

Dim SpanishMonthName as String = Globalization.CultureInfo.GetCultureInfo("ES").DateTimeFormat.GetMonthName(8)

But how can I get the string "August" (8th month name in english) as translation for the spanish or italian month name "agosto"?

PeterCo
  • 910
  • 2
  • 20
  • 36
  • Once you have the number you know how to get "August" from it, so your real question is how to get the month number from a month name when you don't know what language the month name is in, right? Do you have a list of the cultures it could be from? – Ian Mercer Jan 03 '21 at 17:44
  • @IanMercer Unfortunately, I don't have such list of cultures. At least not one including all possible languages .-) – PeterCo Jan 03 '21 at 18:05
  • 1
    `var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures);` – Ian Mercer Jan 03 '21 at 18:34

1 Answers1

3

Inside the various CultureInfo there are the month names (see someCulture.DateTimeFormat.MonthNames), so you could:

var italian = CultureInfo.GetCultureInfo("it-IT");
var spanish = CultureInfo.GetCultureInfo("es-ES");
var english = CultureInfo.GetCultureInfo("en-US");

string month = "agosto";

var italianMonthNames = italian.DateTimeFormat.MonthNames;
var spanishMonthNames = spanish.DateTimeFormat.MonthNames;

int ix = Array.FindIndex(italianMonthNames, x => StringComparer.OrdinalIgnoreCase.Equals(x, month));
if (ix == -1)
{
    ix = Array.FindIndex(spanishMonthNames, x => StringComparer.OrdinalIgnoreCase.Equals(x, month));
}

// ix is 0 based, while months are 1 based
string englishMonth = ix != -1 ? english.DateTimeFormat.GetMonthName(ix + 1) : null;

You could even try to delegate a little to the .NET DateTime.ParseExact:

var italian = CultureInfo.GetCultureInfo("it-IT");
var spanish = CultureInfo.GetCultureInfo("es-ES");
var english = CultureInfo.GetCultureInfo("en-US");

string month = "agosto";
string englishMonth = null;
DateTime dt;

if (DateTime.TryParseExact(month, "MMMM", italian, 0, out dt) || DateTime.TryParseExact(month, "MMMM", spanish, 0, out dt))
{
    englishMonth = dt.ToString("MMMM", english);
}

In general there is at least one month that has a different meaning in two languages: listopad (October or November, see here). The full list is Hlakola, listopad, Mopitlo, Nhlangula, Nyakanga, Phupu

A first version that uses a Dictionary<> to collect month names:

public class MonthNameFinder
{
    private readonly IReadOnlyDictionary<string, int> MonthToNumber;

    public MonthNameFinder(params string[] cultures)
    {
        MonthToNumber = BuildDictionary(cultures.Select(x => CultureInfo.GetCultureInfo(x)));
    }

    public MonthNameFinder(params CultureInfo[] cultureInfos)
    {
        MonthToNumber = BuildDictionary(cultureInfos);
    }

    public MonthNameFinder(CultureTypes cultureTypes = CultureTypes.AllCultures)
    {
        MonthToNumber = BuildDictionary(CultureInfo.GetCultures(cultureTypes));
    }

    private static IReadOnlyDictionary<string, int> BuildDictionary(IEnumerable<CultureInfo> cultureInfos)
    {
        // Note that the comparer will always be wrong, sadly. Each culture has its comparer
        var dict = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);

        foreach (var culture in cultureInfos)
        {
            var monthNames = culture.DateTimeFormat.MonthNames;

            for (int i = 0; i < monthNames.Length; i++)
            {
                string monthName = monthNames[i];

                int other;

                if (!dict.TryGetValue(monthName, out other))
                {
                    dict[monthName] = i + 1;
                }
                else if (other != i + 1)
                {
                    Debug.WriteLine($"Repeated month {monthName}: {i + 1} in {culture.Name} ({culture.DisplayName})");
                }
            }
        }

        return dict;
    }

    public int? GetMonthNumber(string monthName)
    {
        int monthNumber;

        if (MonthToNumber.TryGetValue(monthName, out monthNumber))
        {
            return monthNumber;
        }

        return null;
    }
}

use it like:

var mnf = new MonthNameFinder();
int? n = mnf.GetMonthNumber("agosto");

if (n != null)
{
    string name = new DateTime(1, n.Value, 1).ToString("MMMM", CultureInfo.GetCultureInfo("en-US"));
}

(note that you should cache mnf... it is probably quite expensive to build)

Mmmh... I don't like it... I'm a little OC... and the simple fact that I know that there are some collisions in the name of the months is bothering me.

Here is a second version, using a ILookup<> and saving even the CultureName, so that it is possible to discover the language(s) of the month name. The GetMonthNumbers(monthName) now returns a (int MonthNumber, string CultureName)[], an array of anonymous valuet types. You can clearly take the first one and live happily, or you can check it to see if there are multiple different MonthNumbers.

public class MonthNameFinder
{
    private readonly ILookup<string, (int MonthNumber, string CultureName)> MonthToNumber;

    public MonthNameFinder(params string[] cultures)
    {
        MonthToNumber = BuildLookup(cultures.Select(x => CultureInfo.GetCultureInfo(x)));
    }

    public MonthNameFinder(params CultureInfo[] cultureInfos)
    {
        MonthToNumber = BuildLookup(cultureInfos);
    }

    public MonthNameFinder(CultureTypes cultureTypes = CultureTypes.AllCultures)
    {
        MonthToNumber = BuildLookup(CultureInfo.GetCultures(cultureTypes));
    }

    private static ILookup<string, (int MonthNumber, string CultureName)> BuildLookup(IEnumerable<CultureInfo> cultureInfos)
    {
        // Note that the comparer will always be wrong, sadly. Each culture has its comparer
        var lst = new List<(string Name, int MonthNumber, string CultureName)>();

        foreach (var culture in cultureInfos)
        {
            var monthNames = culture.DateTimeFormat.MonthNames;

            for (int i = 0; i < monthNames.Length; i++)
            {
                string monthName = monthNames[i];
                lst.Add((monthName, i + 1, culture.Name));
            }
        }

        return lst.OrderBy(x => x.Name)
            .ThenBy(x => x.MonthNumber)
            .ToLookup(x => x.Name, x => (x.MonthNumber, x.CultureName), StringComparer.InvariantCultureIgnoreCase);
    }

    public (int MonthNumber, string CultureName)[] GetMonthNumbers(string monthName)
    {
        return MonthToNumber[monthName].ToArray();
    }
}

Use it like:

// This is an array of (MonthNumber, CultureName)
var mnf = new MonthNameFinder();

var numbers = mnf.GetMonthNumbers("agosto");

if (numbers.Length != 0)
{
    string monthName = new DateTime(1, numbers[0].MonthNumber, 1).ToString("MMMM", CultureInfo.GetCultureInfo("en-US"));
}

(even here you should cache mnf... it is probably quite expensive to build)

Note that there are many similar cultures, so numbers will be quite big (for example just for Italian there are 5 cultures, and a search for agosto returned 52 different cultures with the month agosto.

xanatos
  • 109,618
  • 12
  • 197
  • 280
  • Whoa, `Array.FindIndex` twice sounds inefficient. Why not just `Array.Find()`? – Charlieface Jan 03 '21 at 17:52
  • You probably meant `Array.IndexOf`.... With `Array.Find` if I look for _Agosto_ it will return _Agosto_ ... `Array.IndexOf` doesn't support ignore case comparers. If a performant solution is wanted, it is possible to cache the month names in a dictionary. But it becomes more complex. – xanatos Jan 03 '21 at 17:55
  • OK. But why not cache the result in a local? – Charlieface Jan 03 '21 at 17:59
  • @Charlieface Because that's not what's being asked in the question? – Camilo Terevinto Jan 03 '21 at 18:00
  • 1
    @Charlieface The second `Array.FindIndex` is done if the first failed, and is done on **spanish** month names, while the first is done on **italian** month names – xanatos Jan 03 '21 at 18:00
  • Whoops sorry, my bad – Charlieface Jan 03 '21 at 18:02
  • @xanatos Your second example works great and seems to be easier to understand (for me). I assume, there is no way to tell .NET to try all CulturInfo without defining them first for every language individually with "xx-YY"? Just for the case that "agosto" is portuguese? .-) – PeterCo Jan 03 '21 at 18:16
  • 1
    @PeterCo Yes, it is possible, but as I've written in the code, there are some name collisions... _listopad_ for example. A full list of collisions is: Hlakola, listopad, Mopitlo, Nhlangula, Nyakanga, Phupu – xanatos Jan 03 '21 at 18:28
  • @xanatos I think I could live with that. As well as the fact that there are Cultures with 13 months, leap months and so on... – PeterCo Jan 03 '21 at 18:31