dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.62k stars 4.56k forks source link

DateTime.ToString("MMM", CultureInfo.CreateSpecificCulture("DE-de")) returns an extra period in netcore #72061

Closed rhyous closed 2 years ago

rhyous commented 2 years ago

Description

There is a breaking change in DateTime.ToString(string format, IFormatProvider provider) that looks to be a bug. In german, de-DE, it is adding a extra period '.' in net6.0 that isn't added in net4.8

When running the same unit test for DateTime.ToString("dd MMM yyy") net6.0 fails while net4.8 works.

Configuration

Regression?

Other information

How to easily Reproduce

  1. Create a new Unit Test project in VS 2022 as .Net Core
  2. Change the csproj to work with net4.8

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFrameworks>net4.8;net6.0</TargetFrameworks>
    
        <IsPackable>false</IsPackable>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
        <PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
        <PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
        <PackageReference Include="coverlet.collector" Version="3.1.2" />
      </ItemGroup>
    
    </Project>

    Note: I had to unload and reload the project after making this change to get net4.8 portion to build.

  3. Add these tests

    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using System;
    using System.Globalization;
    
    namespace DateTimeToStringDifferenceInDotNetCore
    {
        [TestClass]
        public class UnitTest1
        {
            [TestMethod]
            [DataRow("de-DE", "Jan")]
            public void DateTime_ToString_Differences_Between_Net48_and_Net60_Test1(string language, string expected)
            {
                DateTime dt = new DateTime(2022, 01, 01);
                var actual = dt.ToString("MMM", CultureInfo.CreateSpecificCulture(language));
                Assert.AreEqual(expected, actual);
            }
    
            [TestMethod]
            [DataRow("de-DE", "01 Jan 2022")]
            public void DateTime_ToString_Differences_Between_Net48_and_Net60_Test2(string language, string expected)
            {
                DateTime dt = new DateTime(2022, 01, 01);
                var actual = dt.ToString("dd MMM yyyy", CultureInfo.CreateSpecificCulture(language));
                Assert.AreEqual(expected, actual);
            }
        }
    }
  4. Run the tests.

Notice that net4.8 succeeds both tests while net6.0 fails. There is an extra period where there shouldn't be.

dotnet-issue-labeler[bot] commented 2 years ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

ghost commented 2 years ago

Tagging subscribers to this area: @dotnet/area-system-globalization See info in area-owners.md if you want to be subscribed.

Issue Details
### Description There is a breaking change in DateTime.ToString(string format, IFormatProvider provider) that looks to be a bug. In german, de-DE, it is adding a extra period '.' in net6.0 that isn't added in net4.8 When running the same unit test for DateTime.ToString("dd MMM yyy") net6.0 fails while net4.8 works. ### Configuration * Which version of .NET is the code running on? both net4.8 and net6.0 ``` net4.8;net6.0 ``` * What OS and version, and for Linux, what distro? Windows 10 * What is the architecture (x64, x86, ARM, ARM64)? x64 * Do you know whether it is specific to that configuration? No ### Regression? * Did this work in a previous build or release of .NET, or from .NET Framework? Yes. ### Other information * Do you know of any workarounds? No * Has it been reported by other? I couldn't find an report to this team, but there is a stackoverflow question: https://stackoverflow.com/questions/55571891/c-sharp-net-core-difference-in-date-format-mmm-on-windows-and-linux ### How to easily Reproduce 1. Create a new Unit Test project in VS 2022 as .Net Core 2. Change the csproj to work with net4.8 ``` net4.8;net6.0 false ``` Note: I had to unload and reload the project after making this change to get net4.8 portion to build. 4. Add these tests ```` using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Globalization; namespace DateTimeToStringDifferenceInDotNetCore { [TestClass] public class UnitTest1 { [TestMethod] [DataRow("de-DE", "Jan")] public void DateTime_ToString_Differences_Between_Net48_and_Net60_Test1(string language, string expected) { DateTime dt = new DateTime(2022, 01, 01); var actual = dt.ToString("MMM", CultureInfo.CreateSpecificCulture(language)); Assert.AreEqual(expected, actual); } [TestMethod] [DataRow("de-DE", "01 Jan 2022")] public void DateTime_ToString_Differences_Between_Net48_and_Net60_Test2(string language, string expected) { DateTime dt = new DateTime(2022, 01, 01); var actual = dt.ToString("dd MMM yyyy", CultureInfo.CreateSpecificCulture(language)); Assert.AreEqual(expected, actual); } } } ```` 4. Run the tests. Notice that net4.8 succeeds both tests while net6.0 fails. There is an extra period where there shouldn't be.
Author: rhyous
Assignees: -
Labels: `area-System.Globalization`, `untriaged`
Milestone: -
MichalPetryka commented 2 years ago

Does this reproduce with DOTNET_SYSTEM_GLOBALIZATION_USENLS set to 1?

tarekgh commented 2 years ago

Please have a look at https://github.com/dotnet/runtime/issues/68385#issuecomment-1107152290 for the explanation. I am closing the issue but feel free to send any follow up questions and I'll be happy to help with.

Also, please have a look at the doc https://docs.microsoft.com/en-us/dotnet/core/extensions/globalization-icu for more information regarding the globalization behavior difference between .NET Framework and .NET.

tarekgh commented 2 years ago

Does this reproduce with DOTNET_SYSTEM_GLOBALIZATION_USENLS set to 1?

We don't recommend enabling the NLS mode except if it is crucial to have this behavior. Moving forward, NLS is a legacy behavior which will deviate from the current behavior. Also, will cause inconsistency with other platforms (Linux, MacOS...etc).

rhyous commented 2 years ago

Thank you so much for responding so quickly. I read through the other articles linked. I didn't not try NLS.

Clarifying question: What is the option the doesn't include a period? Or are saying is that the extra period '.' is just going to be there and I have to code a fix for this myself?

If I do have to fix it myself, no biggie. I can do something like this.

var d =  dt.ToString("dd", CultureInfo.CreateSpecificCulture(language));
var m = dt.ToString("MMM", CultureInfo.CreateSpecificCulture(language));
var y = dt.ToString("yyyy", CultureInfo.CreateSpecificCulture(language));
// Any manipulation of d, m, or y needed
var dateString = $"{d} {m} {y}";

However, it seems like the '.' should have it's own ToString() character representation, such as 'p' for period or punctuation. Wouldn't this make more sense:

"dd MMM yyyy" <-- Does not have the period after the month abbreviation "dd MMMp yyyy" <-- Has the period after the month abbreviation

This would make it so if the person wants the period to show, it does, if the person doesn't want it to show, it doesn't. However, I get it. Lots to do and not every feature can make it.

Have a nice day. :-)

Clockwork-Muse commented 2 years ago

When you're dealing with localization, your options are generally:

Clarifying question: What is the option the doesn't include a period? Or are saying is that the extra period '.' is just going to be there and I have to code a fix for this myself?

So... what's the purpose of this part of your application, and which of those two scenarios does it fit in. If you think it's not either, why?

tarekgh commented 2 years ago

Thanks @rhyous for the question. @Clockwork-Muse question to you is good too to answer as you may targeting a scenario that is not desirable by your customer.

Clarifying question: What is the option the doesn't include a period? Or are saying is that the extra period '.' is just going to be there and I have to code a fix for this myself?

The period is included in the abbreviated genitive month names. Think about it as a part of the month's name. We are not manually adding the period to the name. This is part of the de-DE culture. Look at the Unicode CLDR data to see that.

You may choose formatting your date differently to avoid getting the abbreviated genitive month names. Usually, the genitive month names will get used in the formatting when using d formats followed by MMM or MMMM specifiers. something like dd MMM, dd MMMM, d MMM...etc. in Your example var m = dt.ToString("MMM", CultureInfo.CreateSpecificCulture(language)); this is not going to generate genitive month name and you shouldn't get the period anyway. But if you use something var m = dt.ToString("dd MMM", CultureInfo.CreateSpecificCulture(language)); at that time you'll get the period in the month name. You may choose to avoid getting the genitive month names by not using the format patterns that generate the genitive month names. If it is necessary to use such format, you may choose to do that in 2 steps by something like:

var culture = CultureInfo.CreateSpecificCulture(language)
var formattedDate = dt.ToString("dd", culture) + " " + dt.ToString("MMM", culture);

The other idea you can use is to override the genitive month names. here is some sample code:

            CultureInfo ci = new CultureInfo("de-DE");
            ci.DateTimeFormat.AbbreviatedMonthGenitiveNames = new string[] { "Jan", "Feb", "März", "Apr", "Mai", "Juni", "Juli", "Aug", "Sept", "Okt", "Nov", "Dez", "" };
            Console.WriteLine(dt.ToString("dd MMM", ci));